create-nextblock 0.15.10 → 0.16.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/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- 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/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- 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/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- 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/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -0
- package/templates/nextblock-template/app/sitemap.ts +1 -1
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- 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/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- 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/ecommerceRendererLoaders.ts +23 -23
- 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/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- 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/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- 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 +0 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/proxy.ts +240 -20
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
- package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app/cms/blocks/editors/ImageBlockEditor.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import React, { useState, useTransition } from 'react';
|
|
4
|
+
import React, { useRef, useState, useTransition } from 'react';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { Label } from "@nextblock-cms/ui";
|
|
7
7
|
import { Input } from "@nextblock-cms/ui";
|
|
@@ -19,11 +19,25 @@ export type ImageBlockContent = {
|
|
|
19
19
|
height: number | null;
|
|
20
20
|
blur_data_url: string | null;
|
|
21
21
|
};
|
|
22
|
-
import { ImageIcon, X as XIcon, Link as LinkIcon, DownloadCloud } from 'lucide-react';
|
|
22
|
+
import { ImageIcon, X as XIcon, Link as LinkIcon, DownloadCloud, Loader2, Sparkles } from 'lucide-react';
|
|
23
|
+
import { toast } from 'sonner';
|
|
23
24
|
import MediaPickerDialog from "../../media/components/MediaPickerDialog";
|
|
24
25
|
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
25
26
|
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
26
27
|
import { importExternalImageToMedia } from "../../media/import-external-image";
|
|
28
|
+
import { useCortexAiActive } from '../../components/CortexAiActiveContext';
|
|
29
|
+
import { useCortexAiPageContext } from '../../components/CortexAiPageContext';
|
|
30
|
+
import { buildCortexAiRequestHeaders } from '../../../../lib/cortex-ai/sandbox-headers';
|
|
31
|
+
import {
|
|
32
|
+
buildAltTextContext,
|
|
33
|
+
toAbsoluteImageUrl,
|
|
34
|
+
UNRESOLVABLE_IMAGE_URL_MESSAGE,
|
|
35
|
+
} from '../../../../lib/cortex-ai/alt-text-request';
|
|
36
|
+
import {
|
|
37
|
+
altTextImageIdentity,
|
|
38
|
+
resolveAltTextWriteBack,
|
|
39
|
+
STALE_ALT_TEXT_MESSAGE,
|
|
40
|
+
} from '../../../../lib/seo/alt-text-write-back';
|
|
27
41
|
|
|
28
42
|
const deriveAltFromFilename = (name: string) => {
|
|
29
43
|
const lastDot = name.lastIndexOf('.');
|
|
@@ -38,6 +52,30 @@ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps
|
|
|
38
52
|
const [importError, setImportError] = useState<string | null>(null);
|
|
39
53
|
const [isImporting, startImport] = useTransition();
|
|
40
54
|
|
|
55
|
+
// Cortex AI is a premium package. Gating the affordance on activation (rather than
|
|
56
|
+
// letting the button 403) is the difference between a feature the operator has not
|
|
57
|
+
// bought and a feature that looks broken.
|
|
58
|
+
const isCortexAiActive = useCortexAiActive();
|
|
59
|
+
const cortexAiPageContext = useCortexAiPageContext();
|
|
60
|
+
const [isGeneratingAltText, setIsGeneratingAltText] = useState(false);
|
|
61
|
+
// Kept separate from `importError` so an alt-text failure never clears — or is cleared
|
|
62
|
+
// by — an unrelated "save to library" failure sitting a few lines above it.
|
|
63
|
+
const [altTextError, setAltTextError] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Live mirrors of the two values that describe which image this block holds.
|
|
67
|
+
*
|
|
68
|
+
* The alt-text generation below is the only async write-back in this editor, and it is
|
|
69
|
+
* the only place that must read these AFTER an await. Refs updated during render (the
|
|
70
|
+
* same pattern `RobotsCard` uses for its Ctrl+S handler) give that continuation the
|
|
71
|
+
* current values instead of the ones its closure captured when the button was clicked,
|
|
72
|
+
* which is what stops a slow vision response from overwriting a newer edit.
|
|
73
|
+
*/
|
|
74
|
+
const contentRef = useRef(content);
|
|
75
|
+
contentRef.current = content;
|
|
76
|
+
const selectedMediaObjectKeyRef = useRef(selectedMediaObjectKey);
|
|
77
|
+
selectedMediaObjectKeyRef.current = selectedMediaObjectKey;
|
|
78
|
+
|
|
41
79
|
const handleSelectMediaFromLibrary = (mediaItem: Media) => {
|
|
42
80
|
const newAlt = mediaItem.description && mediaItem.description.trim().length > 0
|
|
43
81
|
? mediaItem.description
|
|
@@ -122,6 +160,122 @@ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps
|
|
|
122
160
|
const displayImageUrl = resolveMediaUrl(displayObjectKey);
|
|
123
161
|
const hasImage = Boolean(displayObjectKey || content.external_url);
|
|
124
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Ask Cortex AI to describe the image currently in this block.
|
|
165
|
+
*
|
|
166
|
+
* Three things about this deserve stating. First, the URL: `resolveMediaUrl()` hands back
|
|
167
|
+
* a RELATIVE `/${objectKey}` whenever `NEXT_PUBLIC_R2_BASE_URL` is unset — which is the
|
|
168
|
+
* default on the native Supabase-storage backend — and the alt-text route cannot use
|
|
169
|
+
* that, because the AI SDK downloads the image server-side and a relative path has no
|
|
170
|
+
* base there. `toAbsoluteImageUrl()` resolves it against the origin the CMS is being
|
|
171
|
+
* served from; if the result still is not http(s) (a blob: preview of an unsaved file,
|
|
172
|
+
* say) we say so inline instead of firing a request guaranteed to fail.
|
|
173
|
+
*
|
|
174
|
+
* Second, the write-back: the generated string goes through the exact same
|
|
175
|
+
* `onChange({ ...content, object_key, alt_text })` call the text input uses. Feature 2
|
|
176
|
+
* requires the value to land in the block's JSONB attributes, and the surest way to
|
|
177
|
+
* guarantee that is to make the AI path indistinguishable from typing — same shape,
|
|
178
|
+
* same key set, same downstream save. A bespoke write here would be a second code path
|
|
179
|
+
* to keep in sync forever, for no gain.
|
|
180
|
+
*
|
|
181
|
+
* Third, staleness. This handler used to close over `content` and finish with
|
|
182
|
+
* `onChange({ ...capturedContent, ... })`, on the assumption that the block it started
|
|
183
|
+
* on would still be the block it finished on. It is not: the vision call takes seconds
|
|
184
|
+
* and the editor stays live throughout, so an author who removes the image or picks a
|
|
185
|
+
* different one mid-flight would have had the deleted image restored, or a new image
|
|
186
|
+
* labelled with the previous image's description — alt text that is confidently wrong
|
|
187
|
+
* and undetectable by the screen-reader user it is written for. The image's identity is
|
|
188
|
+
* therefore captured instead of its content, and the response is discarded unless the
|
|
189
|
+
* block still holds that same image. `resolveAltTextWriteBack` owns that rule so it can
|
|
190
|
+
* be unit-tested; this component cannot be, as the workspace has no DOM test env.
|
|
191
|
+
*/
|
|
192
|
+
const handleGenerateAltText = async () => {
|
|
193
|
+
if (!hasImage || isGeneratingAltText) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const sourceUrl = content.external_url || displayImageUrl;
|
|
198
|
+
const imageUrl = toAbsoluteImageUrl(sourceUrl);
|
|
199
|
+
|
|
200
|
+
if (!imageUrl) {
|
|
201
|
+
setAltTextError(UNRESOLVABLE_IMAGE_URL_MESSAGE);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Taken before the await, from the same two values the request itself is built from,
|
|
206
|
+
// so "is this still the image we asked about?" is answerable when the reply lands.
|
|
207
|
+
const capturedIdentity = altTextImageIdentity(content, selectedMediaObjectKey);
|
|
208
|
+
|
|
209
|
+
setAltTextError(null);
|
|
210
|
+
setIsGeneratingAltText(true);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Context materially improves alt text — "Nicolas at the 2026 harvest" beats "a man
|
|
214
|
+
// in a field" — so we pass whatever is already in hand for free: this block's own
|
|
215
|
+
// caption, and the title of the page or post being edited, which the CMS layout
|
|
216
|
+
// already registers in the Cortex AI page context. Nothing is fetched for this.
|
|
217
|
+
const context = buildAltTextContext(
|
|
218
|
+
content.caption ? `Image caption: ${content.caption}` : null,
|
|
219
|
+
cortexAiPageContext?.pageContext?.title
|
|
220
|
+
? `Appears on the ${cortexAiPageContext.pageContext.contentType} “${cortexAiPageContext.pageContext.title}”.`
|
|
221
|
+
: null
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const response = await fetch('/api/ai/seo/alt-text', {
|
|
225
|
+
// The route's body schema is a `z.strictObject` of optionals, so an absent field
|
|
226
|
+
// must be omitted rather than sent as null — a null would fail validation outright.
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
...(context ? { context } : {}),
|
|
229
|
+
imageUrl,
|
|
230
|
+
}),
|
|
231
|
+
headers: buildCortexAiRequestHeaders(),
|
|
232
|
+
method: 'POST',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
236
|
+
|
|
237
|
+
if (!response.ok || !payload) {
|
|
238
|
+
const message =
|
|
239
|
+
payload && typeof payload['error'] === 'string'
|
|
240
|
+
? (payload['error'] as string)
|
|
241
|
+
: 'Cortex AI could not describe this image.';
|
|
242
|
+
throw new Error(message);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const generated = typeof payload['altText'] === 'string' ? payload['altText'].trim() : '';
|
|
246
|
+
if (!generated) {
|
|
247
|
+
throw new Error('Cortex AI returned an empty description.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const currentContent = contentRef.current;
|
|
251
|
+
const currentObjectKey = selectedMediaObjectKeyRef.current;
|
|
252
|
+
const writeBack = resolveAltTextWriteBack({
|
|
253
|
+
capturedIdentity,
|
|
254
|
+
currentContent,
|
|
255
|
+
currentIdentity: altTextImageIdentity(currentContent, currentObjectKey),
|
|
256
|
+
generatedAltText: generated,
|
|
257
|
+
objectKey: currentObjectKey,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!writeBack) {
|
|
261
|
+
// Not thrown: nothing failed. The model answered, the answer is simply about an
|
|
262
|
+
// image this block no longer shows, so it is reported inline and dropped. A toast
|
|
263
|
+
// is skipped for the same reason — this is not an error the operator caused.
|
|
264
|
+
setAltTextError(STALE_ALT_TEXT_MESSAGE);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onChange(writeBack);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const message =
|
|
271
|
+
error instanceof Error ? error.message : 'Cortex AI could not describe this image.';
|
|
272
|
+
setAltTextError(message);
|
|
273
|
+
toast.error(message);
|
|
274
|
+
} finally {
|
|
275
|
+
setIsGeneratingAltText(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
125
279
|
return (
|
|
126
280
|
<div className="space-y-3 p-3 border-t mt-2">
|
|
127
281
|
<Label>Image</Label>
|
|
@@ -217,8 +371,31 @@ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps
|
|
|
217
371
|
</div>
|
|
218
372
|
|
|
219
373
|
<div>
|
|
220
|
-
<
|
|
374
|
+
<div className="flex items-center justify-between gap-2">
|
|
375
|
+
<Label htmlFor={`image-alt-${content.media_id || 'new'}`}>Alt Text</Label>
|
|
376
|
+
{/* Rendered only when the premium package is active, so a non-premium install
|
|
377
|
+
never shows a button that cannot work. */}
|
|
378
|
+
{isCortexAiActive && (
|
|
379
|
+
<Button
|
|
380
|
+
type="button"
|
|
381
|
+
variant="outline"
|
|
382
|
+
size="sm"
|
|
383
|
+
className="h-7 gap-1.5 px-2 text-[11px]"
|
|
384
|
+
onClick={handleGenerateAltText}
|
|
385
|
+
disabled={!hasImage || isGeneratingAltText}
|
|
386
|
+
title={hasImage ? "Describe this image with Cortex AI" : "Select an image first"}
|
|
387
|
+
>
|
|
388
|
+
{isGeneratingAltText ? (
|
|
389
|
+
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
|
|
390
|
+
) : (
|
|
391
|
+
<Sparkles aria-hidden="true" className="h-3.5 w-3.5 text-amber-500" />
|
|
392
|
+
)}
|
|
393
|
+
{isGeneratingAltText ? "Generating…" : "Generate with AI"}
|
|
394
|
+
</Button>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
221
397
|
<Input id={`image-alt-${content.media_id || 'new'}`} value={content.alt_text || ""} onChange={handleAltTextChange} className="mt-1" disabled={!hasImage} />
|
|
398
|
+
{altTextError && <p className="text-xs text-red-500 mt-1">{altTextError}</p>}
|
|
222
399
|
</div>
|
|
223
400
|
<div>
|
|
224
401
|
<Label htmlFor={`image-caption-${content.media_id || 'new'}`}>Caption</Label>
|
|
@@ -3,27 +3,50 @@
|
|
|
3
3
|
|
|
4
4
|
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import dynamic from 'next/dynamic';
|
|
6
|
-
import type { Extensions } from '@tiptap/core';
|
|
6
|
+
import type { Editor, Extensions } from '@tiptap/core';
|
|
7
|
+
import { PanelRightClose, PanelRightOpen } from 'lucide-react';
|
|
7
8
|
import MediaPickerDialog from '../../media/components/MediaPickerDialog';
|
|
8
|
-
import { Label } from '@nextblock-cms/ui';
|
|
9
|
+
import { Button, Label } from '@nextblock-cms/ui';
|
|
9
10
|
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
10
11
|
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
11
12
|
import { useCortexAiActive } from '../../components/CortexAiActiveContext';
|
|
13
|
+
import { SeoAuditPanel } from '../../../../components/seo/SeoAuditPanel';
|
|
14
|
+
import { usePageSeo } from '../../../../lib/seo/page-audit-context';
|
|
12
15
|
import {
|
|
13
16
|
createDynamicCustomBlockExtensions,
|
|
14
17
|
type DynamicCustomBlockEditorDefinition,
|
|
15
18
|
} from '../../../../lib/editor/dynamic-extensions';
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A hand-maintained copy of the real `NotionEditorProps`.
|
|
22
|
+
*
|
|
23
|
+
* `next/dynamic` needs a concrete type argument at the call site, and the real
|
|
24
|
+
* interface lives behind a lazy `import()` that TypeScript will not unwrap for
|
|
25
|
+
* us here. That makes this a second, independent declaration of the same
|
|
26
|
+
* contract: a prop added to `libs/editor/src/lib/NotionEditor.tsx` and not added
|
|
27
|
+
* here is invisible to this file and fails to type-check the moment it is
|
|
28
|
+
* passed. Both `onEditorReady` and `sidePanel` below exist for that reason.
|
|
29
|
+
*/
|
|
18
30
|
type NotionEditorProps = {
|
|
19
31
|
content: string;
|
|
20
32
|
onChange: (html: string) => void;
|
|
33
|
+
onEditorReady?: (editor: Editor | null) => void;
|
|
21
34
|
openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
|
|
22
35
|
className?: string;
|
|
23
36
|
showAiPrompt?: boolean;
|
|
37
|
+
sidePanel?: React.ReactNode;
|
|
24
38
|
dynamicExtensions?: Extensions;
|
|
25
39
|
};
|
|
26
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Where the "is the SEO panel open?" choice is remembered.
|
|
43
|
+
*
|
|
44
|
+
* Per-browser rather than per-user or per-block: it is a workspace preference in
|
|
45
|
+
* the same family as a collapsed sidebar, not content, and round-tripping it
|
|
46
|
+
* through the database would mean a write on every toggle.
|
|
47
|
+
*/
|
|
48
|
+
const SEO_PANEL_STORAGE_KEY = 'nextblock_seo_audit_panel_open';
|
|
49
|
+
|
|
27
50
|
// Use the alias that resolves in your repo; if you mapped @nextblock-cms/editor, swap it here.
|
|
28
51
|
const NotionEditor = dynamic<NotionEditorProps>(
|
|
29
52
|
() => import('@nextblock-cms/editor').then((m) => m.NotionEditor),
|
|
@@ -44,7 +67,88 @@ export default function TextBlockEditor({
|
|
|
44
67
|
const [dynamicDefinitions, setDynamicDefinitions] = useState<DynamicCustomBlockEditorDefinition[]>([]);
|
|
45
68
|
const [dynamicDefinitionsLoaded, setDynamicDefinitionsLoaded] = useState(false);
|
|
46
69
|
const isCortexAiActive = useCortexAiActive();
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The live Tiptap instance, published by `NotionEditor` through
|
|
73
|
+
* `onEditorReady`. This is what lets the SEO panel write a Cortex AI fix back
|
|
74
|
+
* into the document; without it the panel still grades the content but shows
|
|
75
|
+
* no fix buttons. Deliberately not `window.__nextblockEditor`, which is a
|
|
76
|
+
* single global slot and therefore wrong as soon as a second editor mounts.
|
|
77
|
+
*/
|
|
78
|
+
const [editor, setEditor] = useState<Editor | null>(null);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The focus keyphrase, preferring the page's copy when there is one.
|
|
82
|
+
*
|
|
83
|
+
* A keyphrase is a property of the page, not of a paragraph, so when this
|
|
84
|
+
* editor is opened from a page or post edit screen it reads and writes the
|
|
85
|
+
* shared `PageSeoProvider` value: type it once on the page panel and every
|
|
86
|
+
* block editor opened afterwards is already grading against it, and typing it
|
|
87
|
+
* in a block sends it back the other way. The local state below is the
|
|
88
|
+
* fallback for surfaces with no page-level panel — the product editor — where
|
|
89
|
+
* an ephemeral per-block keyphrase is still better than a dead field.
|
|
90
|
+
*/
|
|
91
|
+
const pageSeo = usePageSeo();
|
|
92
|
+
const [localSeoKeyword, setLocalSeoKeyword] = useState('');
|
|
93
|
+
const seoKeyword = pageSeo ? pageSeo.keyword : localSeoKeyword;
|
|
94
|
+
const handleSeoKeywordChange = useCallback(
|
|
95
|
+
(next: string) => {
|
|
96
|
+
if (pageSeo) {
|
|
97
|
+
pageSeo.setKeyword(next);
|
|
98
|
+
} else {
|
|
99
|
+
setLocalSeoKeyword(next);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[pageSeo]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Starts closed for the very first render on the server and on a cold client,
|
|
106
|
+
// then adopts the stored preference in the effect below. Reading storage in
|
|
107
|
+
// the initialiser instead would make the server's HTML and the client's first
|
|
108
|
+
// render disagree for anyone who had opened the panel before.
|
|
109
|
+
const [isSeoPanelOpen, setIsSeoPanelOpen] = useState(false);
|
|
110
|
+
|
|
47
111
|
const resolverRef = useRef<null | ((v: any) => void)>(null);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Adopt the remembered preference once, after mount.
|
|
115
|
+
*
|
|
116
|
+
* Every access is wrapped because `localStorage` is not merely empty in a
|
|
117
|
+
* private window or with site data blocked — the property access itself
|
|
118
|
+
* throws, and an uncaught throw here would take the whole block editor down
|
|
119
|
+
* over a panel toggle. Defaulting to open on a first visit is deliberate: the
|
|
120
|
+
* feature is invisible otherwise, and one click hides it for good afterwards.
|
|
121
|
+
*/
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
try {
|
|
124
|
+
const stored = window.localStorage.getItem(SEO_PANEL_STORAGE_KEY);
|
|
125
|
+
setIsSeoPanelOpen(stored === null ? true : stored === 'true');
|
|
126
|
+
} catch {
|
|
127
|
+
setIsSeoPanelOpen(true);
|
|
128
|
+
}
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const toggleSeoPanel = useCallback(() => {
|
|
132
|
+
setIsSeoPanelOpen((previous) => {
|
|
133
|
+
const next = !previous;
|
|
134
|
+
try {
|
|
135
|
+
window.localStorage.setItem(SEO_PANEL_STORAGE_KEY, String(next));
|
|
136
|
+
} catch {
|
|
137
|
+
// The preference is a convenience; losing it is not worth an error.
|
|
138
|
+
}
|
|
139
|
+
return next;
|
|
140
|
+
});
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wrapped rather than passing `setEditor` straight in. A state setter treats a
|
|
145
|
+
* function argument as an updater callback, and while a Tiptap `Editor` is an
|
|
146
|
+
* object today, routing it through an explicit assignment means this cannot
|
|
147
|
+
* quietly break if that ever stops being true.
|
|
148
|
+
*/
|
|
149
|
+
const handleEditorReady = useCallback((instance: Editor | null) => {
|
|
150
|
+
setEditor(instance);
|
|
151
|
+
}, []);
|
|
48
152
|
const openImagePicker = useCallback(() => {
|
|
49
153
|
setPickerOpen(true);
|
|
50
154
|
return new Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>((resolve) => {
|
|
@@ -103,26 +207,87 @@ export default function TextBlockEditor({
|
|
|
103
207
|
};
|
|
104
208
|
}, []);
|
|
105
209
|
|
|
210
|
+
// The one string the editor and the audit both read. Deriving it once keeps
|
|
211
|
+
// the panel from ever grading something other than what is on screen.
|
|
212
|
+
const htmlContent = content?.html_content ?? '';
|
|
213
|
+
|
|
106
214
|
return (
|
|
107
215
|
<div className="h-full flex flex-col">
|
|
108
216
|
<Label htmlFor={labelId} className="sr-only">
|
|
109
217
|
Text Content
|
|
110
218
|
</Label>
|
|
111
219
|
|
|
220
|
+
<div className="mb-2 flex items-center justify-end">
|
|
221
|
+
<Button
|
|
222
|
+
aria-pressed={isSeoPanelOpen}
|
|
223
|
+
className="h-7 text-xs"
|
|
224
|
+
disabled={!dynamicDefinitionsLoaded}
|
|
225
|
+
onClick={toggleSeoPanel}
|
|
226
|
+
size="sm"
|
|
227
|
+
type="button"
|
|
228
|
+
variant="outline"
|
|
229
|
+
>
|
|
230
|
+
{isSeoPanelOpen ? (
|
|
231
|
+
<PanelRightClose className="mr-1.5 h-4 w-4" />
|
|
232
|
+
) : (
|
|
233
|
+
<PanelRightOpen className="mr-1.5 h-4 w-4" />
|
|
234
|
+
)}
|
|
235
|
+
{isSeoPanelOpen ? 'Hide SEO analysis' : 'Show SEO analysis'}
|
|
236
|
+
</Button>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
112
239
|
<div id={labelId} role="group" aria-labelledby={labelId} className="flex-1 min-h-0 flex flex-col">
|
|
113
240
|
{dynamicDefinitionsLoaded ? (
|
|
114
241
|
<NotionEditor
|
|
115
242
|
key={dynamicDefinitions.map((definition) => definition.id).join('|') || 'static'}
|
|
116
|
-
content={
|
|
243
|
+
content={htmlContent}
|
|
117
244
|
dynamicExtensions={dynamicExtensions}
|
|
118
245
|
onChange={(html) => onChange({ html_content: html })}
|
|
246
|
+
onEditorReady={handleEditorReady}
|
|
119
247
|
openImagePicker={openImagePicker}
|
|
120
248
|
className={className}
|
|
121
249
|
showAiPrompt={isCortexAiActive}
|
|
250
|
+
/*
|
|
251
|
+
* Passing `undefined` rather than a hidden panel is what keeps the
|
|
252
|
+
* collapsed state honest: `NotionEditor` renders its original
|
|
253
|
+
* single-column tree when `sidePanel` is absent, so hiding the panel
|
|
254
|
+
* gives the prose the full width back instead of leaving an empty
|
|
255
|
+
* column behind it.
|
|
256
|
+
*/
|
|
257
|
+
sidePanel={
|
|
258
|
+
isSeoPanelOpen ? (
|
|
259
|
+
/*
|
|
260
|
+
* Block scope, and this is the fix for the complaint that
|
|
261
|
+
* started all of this: mounted here the panel can only see one
|
|
262
|
+
* block, so grading it as a page reported "no H1" and "fewer
|
|
263
|
+
* than 300 words" for a single paragraph — neither of which a
|
|
264
|
+
* block can be responsible for. The page-wide checks now live on
|
|
265
|
+
* the page editor's own panel; what stays here are the checks a
|
|
266
|
+
* block genuinely owns, plus the one thing this mount has and
|
|
267
|
+
* the page mount never will: a live editor instance, which is
|
|
268
|
+
* what makes "Fix with Cortex AI" able to write anything at all.
|
|
269
|
+
*/
|
|
270
|
+
<SeoAuditPanel
|
|
271
|
+
content={htmlContent}
|
|
272
|
+
editor={editor}
|
|
273
|
+
isCortexAiActive={isCortexAiActive}
|
|
274
|
+
keyword={seoKeyword}
|
|
275
|
+
onKeywordChange={handleSeoKeywordChange}
|
|
276
|
+
scope="block"
|
|
277
|
+
/>
|
|
278
|
+
) : undefined
|
|
279
|
+
}
|
|
122
280
|
/>
|
|
123
281
|
) : (
|
|
124
|
-
|
|
125
|
-
|
|
282
|
+
/*
|
|
283
|
+
* The editor is `ssr: false` and gated behind the custom-block
|
|
284
|
+
* definition fetch, so neither it nor the SEO panel exists on first
|
|
285
|
+
* paint. Saying which pieces are still coming makes the gap read as a
|
|
286
|
+
* load rather than as a panel that failed to appear.
|
|
287
|
+
*/
|
|
288
|
+
<div className="flex min-h-[500px] flex-1 flex-col items-center justify-center gap-1 rounded-lg border bg-background text-sm text-muted-foreground">
|
|
289
|
+
<span>Loading editor...</span>
|
|
290
|
+
<span className="text-xs">Custom blocks and the SEO analysis appear once it is ready.</span>
|
|
126
291
|
</div>
|
|
127
292
|
)}
|
|
128
293
|
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import Link from 'next/link';
|
|
4
|
-
import { useState } from 'react';
|
|
5
|
-
import { Button } from '@nextblock-cms/ui';
|
|
6
|
-
import { MailWarning, X } from 'lucide-react';
|
|
7
|
-
import { useAuth } from '../../../context/AuthContext';
|
|
8
|
-
import type { ContactReminder } from '../../../lib/cms/contact-reminder';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Shown when a contact form is addressed to a placeholder, or when nothing resolves to a
|
|
12
|
-
* deliverable address at all.
|
|
13
|
-
*
|
|
14
|
-
* Worth interrupting for, because this failure is completely silent: the visitor is
|
|
15
|
-
* thanked, the mail server accepts the message, and it goes to a reserved domain that can
|
|
16
|
-
* never receive it. The messages are safely stored either way — the problem is that
|
|
17
|
-
* nobody is being *told* they arrived.
|
|
18
|
-
*/
|
|
19
|
-
export default function ContactReminderBanner({ reminder }: { reminder: ContactReminder }) {
|
|
20
|
-
const { isAdmin } = useAuth();
|
|
21
|
-
const [dismissed, setDismissed] = useState(false);
|
|
22
|
-
|
|
23
|
-
if (!isAdmin || dismissed) return null;
|
|
24
|
-
|
|
25
|
-
const { placeholderForms, noFallback } = reminder;
|
|
26
|
-
const first = placeholderForms[0];
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-200">
|
|
30
|
-
<MailWarning className="mt-0.5 h-5 w-5 shrink-0" />
|
|
31
|
-
<div className="min-w-0 flex-1">
|
|
32
|
-
<p className="text-sm font-medium">
|
|
33
|
-
{placeholderForms.length > 0
|
|
34
|
-
? 'Your contact form is still using the example address.'
|
|
35
|
-
: 'Nobody is being notified when someone contacts you.'}
|
|
36
|
-
</p>
|
|
37
|
-
<p className="text-xs opacity-90">
|
|
38
|
-
{placeholderForms.length > 0 ? (
|
|
39
|
-
<>
|
|
40
|
-
“{first?.label}” sends to <strong>{first?.recipient}</strong>, a
|
|
41
|
-
reserved address that can never receive mail
|
|
42
|
-
{placeholderForms.length > 1
|
|
43
|
-
? ` (and ${placeholderForms.length - 1} other form${
|
|
44
|
-
placeholderForms.length > 2 ? 's' : ''
|
|
45
|
-
} like it)`
|
|
46
|
-
: ''}
|
|
47
|
-
. {noFallback ? 'No fallback address is set either. ' : ''}
|
|
48
|
-
Messages are still saved under Messages — but no one is told they arrived.
|
|
49
|
-
</>
|
|
50
|
-
) : (
|
|
51
|
-
<>
|
|
52
|
-
No contact address is configured anywhere, so notifications have nowhere to
|
|
53
|
-
go. Messages are still saved under Messages — but no one is told they
|
|
54
|
-
arrived.
|
|
55
|
-
</>
|
|
56
|
-
)}
|
|
57
|
-
</p>
|
|
58
|
-
</div>
|
|
59
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
60
|
-
<Button asChild size="sm" variant="outline" className="border-amber-400">
|
|
61
|
-
<Link href="/cms/messages">Set an address</Link>
|
|
62
|
-
</Button>
|
|
63
|
-
<Button
|
|
64
|
-
variant="ghost"
|
|
65
|
-
size="icon"
|
|
66
|
-
onClick={() => setDismissed(true)}
|
|
67
|
-
aria-label="Dismiss for now"
|
|
68
|
-
className="h-8 w-8"
|
|
69
|
-
>
|
|
70
|
-
<X className="h-4 w-4" />
|
|
71
|
-
</Button>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { Button } from '@nextblock-cms/ui';
|
|
6
|
+
import { MailWarning, X } from 'lucide-react';
|
|
7
|
+
import { useAuth } from '../../../context/AuthContext';
|
|
8
|
+
import type { ContactReminder } from '../../../lib/cms/contact-reminder';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shown when a contact form is addressed to a placeholder, or when nothing resolves to a
|
|
12
|
+
* deliverable address at all.
|
|
13
|
+
*
|
|
14
|
+
* Worth interrupting for, because this failure is completely silent: the visitor is
|
|
15
|
+
* thanked, the mail server accepts the message, and it goes to a reserved domain that can
|
|
16
|
+
* never receive it. The messages are safely stored either way — the problem is that
|
|
17
|
+
* nobody is being *told* they arrived.
|
|
18
|
+
*/
|
|
19
|
+
export default function ContactReminderBanner({ reminder }: { reminder: ContactReminder }) {
|
|
20
|
+
const { isAdmin } = useAuth();
|
|
21
|
+
const [dismissed, setDismissed] = useState(false);
|
|
22
|
+
|
|
23
|
+
if (!isAdmin || dismissed) return null;
|
|
24
|
+
|
|
25
|
+
const { placeholderForms, noFallback } = reminder;
|
|
26
|
+
const first = placeholderForms[0];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-200">
|
|
30
|
+
<MailWarning className="mt-0.5 h-5 w-5 shrink-0" />
|
|
31
|
+
<div className="min-w-0 flex-1">
|
|
32
|
+
<p className="text-sm font-medium">
|
|
33
|
+
{placeholderForms.length > 0
|
|
34
|
+
? 'Your contact form is still using the example address.'
|
|
35
|
+
: 'Nobody is being notified when someone contacts you.'}
|
|
36
|
+
</p>
|
|
37
|
+
<p className="text-xs opacity-90">
|
|
38
|
+
{placeholderForms.length > 0 ? (
|
|
39
|
+
<>
|
|
40
|
+
“{first?.label}” sends to <strong>{first?.recipient}</strong>, a
|
|
41
|
+
reserved address that can never receive mail
|
|
42
|
+
{placeholderForms.length > 1
|
|
43
|
+
? ` (and ${placeholderForms.length - 1} other form${
|
|
44
|
+
placeholderForms.length > 2 ? 's' : ''
|
|
45
|
+
} like it)`
|
|
46
|
+
: ''}
|
|
47
|
+
. {noFallback ? 'No fallback address is set either. ' : ''}
|
|
48
|
+
Messages are still saved under Messages — but no one is told they arrived.
|
|
49
|
+
</>
|
|
50
|
+
) : (
|
|
51
|
+
<>
|
|
52
|
+
No contact address is configured anywhere, so notifications have nowhere to
|
|
53
|
+
go. Messages are still saved under Messages — but no one is told they
|
|
54
|
+
arrived.
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
60
|
+
<Button asChild size="sm" variant="outline" className="border-amber-400">
|
|
61
|
+
<Link href="/cms/messages">Set an address</Link>
|
|
62
|
+
</Button>
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="icon"
|
|
66
|
+
onClick={() => setDismissed(true)}
|
|
67
|
+
aria-label="Dismiss for now"
|
|
68
|
+
className="h-8 w-8"
|
|
69
|
+
>
|
|
70
|
+
<X className="h-4 w-4" />
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { createContext, useContext } from "react";
|
|
4
|
-
|
|
5
|
-
const CortexAiActiveContext = createContext<boolean>(false);
|
|
6
|
-
|
|
7
|
-
export function CortexAiActiveProvider({
|
|
8
|
-
children,
|
|
9
|
-
isActive,
|
|
10
|
-
}: {
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
isActive: boolean;
|
|
13
|
-
}) {
|
|
14
|
-
return (
|
|
15
|
-
<CortexAiActiveContext.Provider value={isActive}>
|
|
16
|
-
{children}
|
|
17
|
-
</CortexAiActiveContext.Provider>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function useCortexAiActive() {
|
|
22
|
-
return useContext(CortexAiActiveContext);
|
|
23
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
const CortexAiActiveContext = createContext<boolean>(false);
|
|
6
|
+
|
|
7
|
+
export function CortexAiActiveProvider({
|
|
8
|
+
children,
|
|
9
|
+
isActive,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<CortexAiActiveContext.Provider value={isActive}>
|
|
16
|
+
{children}
|
|
17
|
+
</CortexAiActiveContext.Provider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useCortexAiActive() {
|
|
22
|
+
return useContext(CortexAiActiveContext);
|
|
23
|
+
}
|