create-nextblock 0.16.1 → 0.16.3
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 -221
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
- 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/route.ts +4 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
- 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 -558
- 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 +406 -406
- 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 +254 -254
- 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/SeoScoreBadge.tsx +40 -0
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
- 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/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/components/PageForm.tsx +649 -649
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
- 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 -514
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
- 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 -123
- 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 -137
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
- 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 -86
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
- 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 -154
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
- package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
- package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
- 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 +1 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- 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/components/blocks/renderers/TestimonialBlockRenderer.tsx
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
|
|
3
|
-
import { Card, CardContent } from "@nextblock-cms/ui";
|
|
4
|
-
import { MessageSquareQuote } from "lucide-react";
|
|
5
|
-
import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
|
|
6
|
-
|
|
7
|
-
type TestimonialBlockContent = {
|
|
8
|
-
quote?: string;
|
|
9
|
-
author_name?: string;
|
|
10
|
-
author_title?: string;
|
|
11
|
-
image_url?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
interface TestimonialBlockRendererProps {
|
|
15
|
-
content: TestimonialBlockContent;
|
|
16
|
-
visualEditAttributes?: VisualEditAttributes;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const TestimonialBlockRenderer: React.FC<TestimonialBlockRendererProps> = ({
|
|
20
|
-
content,
|
|
21
|
-
visualEditAttributes,
|
|
22
|
-
}) => {
|
|
23
|
-
const authorName = content.author_name || "Customer";
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="container m-8" {...visualEditAttributes}>
|
|
27
|
-
<Card className="h-full">
|
|
28
|
-
<CardContent className="pt-6 flex flex-col gap-4 h-full">
|
|
29
|
-
<MessageSquareQuote className="w-8 h-8 text-primary/40" />
|
|
30
|
-
{content.quote && (
|
|
31
|
-
<blockquote className="flex-grow text-lg italic text-muted-foreground">
|
|
32
|
-
"{content.quote}"
|
|
33
|
-
</blockquote>
|
|
34
|
-
)}
|
|
35
|
-
<div className="flex items-center gap-3 mt-4">
|
|
36
|
-
<Avatar>
|
|
37
|
-
{content.image_url && (
|
|
38
|
-
<AvatarImage src={content.image_url} alt={authorName} />
|
|
39
|
-
)}
|
|
40
|
-
<AvatarFallback>{authorName.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
41
|
-
</Avatar>
|
|
42
|
-
<div>
|
|
43
|
-
<div className="font-semibold">{authorName}</div>
|
|
44
|
-
{content.author_title && (
|
|
45
|
-
<div className="text-sm text-muted-foreground">
|
|
46
|
-
{content.author_title}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
</CardContent>
|
|
52
|
-
</Card>
|
|
53
|
-
</div>
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export default TestimonialBlockRenderer;
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
|
|
3
|
+
import { Card, CardContent } from "@nextblock-cms/ui";
|
|
4
|
+
import { MessageSquareQuote } from "lucide-react";
|
|
5
|
+
import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
|
|
6
|
+
|
|
7
|
+
type TestimonialBlockContent = {
|
|
8
|
+
quote?: string;
|
|
9
|
+
author_name?: string;
|
|
10
|
+
author_title?: string;
|
|
11
|
+
image_url?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface TestimonialBlockRendererProps {
|
|
15
|
+
content: TestimonialBlockContent;
|
|
16
|
+
visualEditAttributes?: VisualEditAttributes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TestimonialBlockRenderer: React.FC<TestimonialBlockRendererProps> = ({
|
|
20
|
+
content,
|
|
21
|
+
visualEditAttributes,
|
|
22
|
+
}) => {
|
|
23
|
+
const authorName = content.author_name || "Customer";
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="container m-8" {...visualEditAttributes}>
|
|
27
|
+
<Card className="h-full">
|
|
28
|
+
<CardContent className="pt-6 flex flex-col gap-4 h-full">
|
|
29
|
+
<MessageSquareQuote className="w-8 h-8 text-primary/40" />
|
|
30
|
+
{content.quote && (
|
|
31
|
+
<blockquote className="flex-grow text-lg italic text-muted-foreground">
|
|
32
|
+
"{content.quote}"
|
|
33
|
+
</blockquote>
|
|
34
|
+
)}
|
|
35
|
+
<div className="flex items-center gap-3 mt-4">
|
|
36
|
+
<Avatar>
|
|
37
|
+
{content.image_url && (
|
|
38
|
+
<AvatarImage src={content.image_url} alt={authorName} />
|
|
39
|
+
)}
|
|
40
|
+
<AvatarFallback>{authorName.slice(0, 2).toUpperCase()}</AvatarFallback>
|
|
41
|
+
</Avatar>
|
|
42
|
+
<div>
|
|
43
|
+
<div className="font-semibold">{authorName}</div>
|
|
44
|
+
{content.author_title && (
|
|
45
|
+
<div className="text-sm text-muted-foreground">
|
|
46
|
+
{content.author_title}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</CardContent>
|
|
52
|
+
</Card>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default TestimonialBlockRenderer;
|
package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface AlertWidgetRendererProps {
|
|
1
|
+
interface AlertWidgetRendererProps {
|
|
2
2
|
type: 'info' | 'warning' | 'notification' | 'danger';
|
|
3
3
|
title: string;
|
|
4
4
|
message: string;
|
|
@@ -46,4 +46,4 @@ const AlertWidgetRenderer = ({ type, title, message, align, size, textAlign }: A
|
|
|
46
46
|
);
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
export default AlertWidgetRenderer;
|
|
49
|
+
export default AlertWidgetRenderer;
|
package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Link from 'next/link';
|
|
1
|
+
import Link from 'next/link';
|
|
2
2
|
|
|
3
3
|
interface CtaWidgetRendererProps {
|
|
4
4
|
text: string;
|
|
@@ -35,4 +35,4 @@ const CtaWidgetRenderer = ({ text, url, style, size, textAlign }: CtaWidgetRende
|
|
|
35
35
|
);
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export default CtaWidgetRenderer;
|
|
38
|
+
export default CtaWidgetRenderer;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Database } from '@nextblock-cms/db';
|
|
2
2
|
|
|
3
|
-
export type PostWithMediaDimensions = Database['public']['Tables']['posts']['Row'] & {
|
|
4
|
-
feature_image_url: string | null;
|
|
5
|
-
feature_image_width: number | null;
|
|
6
|
-
feature_image_height: number | null;
|
|
7
|
-
blur_data_url: string | null;
|
|
8
|
-
estimated_read_time_minutes: number;
|
|
9
|
-
};
|
|
3
|
+
export type PostWithMediaDimensions = Database['public']['Tables']['posts']['Row'] & {
|
|
4
|
+
feature_image_url: string | null;
|
|
5
|
+
feature_image_width: number | null;
|
|
6
|
+
feature_image_height: number | null;
|
|
7
|
+
blur_data_url: string | null;
|
|
8
|
+
estimated_read_time_minutes: number;
|
|
9
|
+
};
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
2
|
-
import { PaymentReadinessProvider } from '@nextblock-cms/ecommerce/PaymentReadinessProvider';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Supplies payment readiness to the buy controls beneath it.
|
|
6
|
-
*
|
|
7
|
-
* Deliberately NOT mounted in the root layout: readiness is only consulted by buy CTAs
|
|
8
|
-
* and the checkout, so a blog post or a landing page should not pay for the lookup. Each
|
|
9
|
-
* commerce block renderer wraps itself in this instead, which also means nested cases
|
|
10
|
-
* (a product grid inside a section) are covered without anyone having to walk the block
|
|
11
|
-
* tree looking for them.
|
|
12
|
-
*
|
|
13
|
-
* getStoreReadiness() is request-cached, so several of these on one page cost one read.
|
|
14
|
-
* Any failure yields no readiness at all, and the context default ("everything ready")
|
|
15
|
-
* keeps a working store working.
|
|
16
|
-
*/
|
|
17
|
-
export default async function PaymentReadinessBoundary({
|
|
18
|
-
children,
|
|
19
|
-
}: {
|
|
20
|
-
children: React.ReactNode;
|
|
21
|
-
}) {
|
|
22
|
-
let readiness: { stripe: boolean; freemius: boolean } | null = null;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const store = await getStoreReadiness();
|
|
26
|
-
readiness = { stripe: store.stripe.ready, freemius: store.freemius.ready };
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error('[PaymentReadinessBoundary] Could not resolve payment readiness:', error);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return <PaymentReadinessProvider readiness={readiness}>{children}</PaymentReadinessProvider>;
|
|
32
|
-
}
|
|
1
|
+
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
2
|
+
import { PaymentReadinessProvider } from '@nextblock-cms/ecommerce/PaymentReadinessProvider';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Supplies payment readiness to the buy controls beneath it.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT mounted in the root layout: readiness is only consulted by buy CTAs
|
|
8
|
+
* and the checkout, so a blog post or a landing page should not pay for the lookup. Each
|
|
9
|
+
* commerce block renderer wraps itself in this instead, which also means nested cases
|
|
10
|
+
* (a product grid inside a section) are covered without anyone having to walk the block
|
|
11
|
+
* tree looking for them.
|
|
12
|
+
*
|
|
13
|
+
* getStoreReadiness() is request-cached, so several of these on one page cost one read.
|
|
14
|
+
* Any failure yields no readiness at all, and the context default ("everything ready")
|
|
15
|
+
* keeps a working store working.
|
|
16
|
+
*/
|
|
17
|
+
export default async function PaymentReadinessBoundary({
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
let readiness: { stripe: boolean; freemius: boolean } | null = null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const store = await getStoreReadiness();
|
|
26
|
+
readiness = { stripe: store.stripe.ready, freemius: store.freemius.ready };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('[PaymentReadinessBoundary] Could not resolve payment readiness:', error);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <PaymentReadinessProvider readiness={readiness}>{children}</PaymentReadinessProvider>;
|
|
32
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { Badge } from "@nextblock-cms/ui";
|
|
3
|
-
import { Button } from "@nextblock-cms/ui";
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
4
|
|
|
5
5
|
export function EnvVarWarning() {
|
|
6
6
|
return (
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useTranslations } from "@nextblock-cms/utils";
|
|
4
|
-
|
|
5
|
-
export type Message =
|
|
6
|
-
| { success: string }
|
|
7
|
-
| { error: string }
|
|
8
|
-
| { message: string };
|
|
9
|
-
|
|
10
|
-
export function FormMessage({ message }: { message?: Message }) {
|
|
11
|
-
const { t } = useTranslations();
|
|
12
|
-
|
|
13
|
-
if (!message) return null;
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<div className="flex flex-col gap-2 w-full max-w-md text-sm">
|
|
17
|
-
{"success" in message && message.success && (
|
|
18
|
-
<div className="text-foreground border-l-2 border-foreground px-4">
|
|
19
|
-
{t(message.success)}
|
|
20
|
-
</div>
|
|
21
|
-
)}
|
|
22
|
-
{"error" in message && message.error && (
|
|
23
|
-
<div className="text-destructive border-l-2 border-destructive px-4">
|
|
24
|
-
{t(message.error)}
|
|
25
|
-
</div>
|
|
26
|
-
)}
|
|
27
|
-
{"message" in message && message.message && (
|
|
28
|
-
<div className="text-foreground border-l-2 px-4">{t(message.message)}</div>
|
|
29
|
-
)}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "@nextblock-cms/utils";
|
|
4
|
+
|
|
5
|
+
export type Message =
|
|
6
|
+
| { success: string }
|
|
7
|
+
| { error: string }
|
|
8
|
+
| { message: string };
|
|
9
|
+
|
|
10
|
+
export function FormMessage({ message }: { message?: Message }) {
|
|
11
|
+
const { t } = useTranslations();
|
|
12
|
+
|
|
13
|
+
if (!message) return null;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col gap-2 w-full max-w-md text-sm">
|
|
17
|
+
{"success" in message && message.success && (
|
|
18
|
+
<div className="text-foreground border-l-2 border-foreground px-4">
|
|
19
|
+
{t(message.success)}
|
|
20
|
+
</div>
|
|
21
|
+
)}
|
|
22
|
+
{"error" in message && message.error && (
|
|
23
|
+
<div className="text-destructive border-l-2 border-destructive px-4">
|
|
24
|
+
{t(message.error)}
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
{"message" in message && message.message && (
|
|
28
|
+
<div className="text-foreground border-l-2 px-4">{t(message.message)}</div>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { Loader2, Sparkles } from 'lucide-react';
|
|
5
|
-
import { toast } from 'sonner';
|
|
6
|
-
import { Button } from '@nextblock-cms/ui';
|
|
7
|
-
|
|
8
|
-
import { buildCortexAiRequestHeaders } from '../../lib/cortex-ai/sandbox-headers';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The four strings `/api/ai/seo/metadata` writes back.
|
|
12
|
-
*
|
|
13
|
-
* Note what is NOT here: an image. The Open Graph image on a page or post is derived from
|
|
14
|
-
* `feature_image_id`, and nothing in this flow changes that. The model writes copy; the
|
|
15
|
-
* operator picks the picture.
|
|
16
|
-
*/
|
|
17
|
-
export interface GeneratedSeoMetadata {
|
|
18
|
-
metaDescription: string;
|
|
19
|
-
metaTitle: string;
|
|
20
|
-
ogDescription: string;
|
|
21
|
-
ogTitle: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface GenerateMetaButtonProps {
|
|
25
|
-
/**
|
|
26
|
-
* Body prose the model reads in order to write about the page. The caller flattens its
|
|
27
|
-
* blocks; an empty string disables the button, because a model asked to summarize
|
|
28
|
-
* nothing invents something, and invented metadata is worse than none.
|
|
29
|
-
*/
|
|
30
|
-
content: string;
|
|
31
|
-
/** Optional term the copy should be built around, when the caller knows one. */
|
|
32
|
-
focusKeyword?: string | null;
|
|
33
|
-
/** Language code (`en`, `fr`, …) so the copy comes back in the content's language. */
|
|
34
|
-
locale?: string | null;
|
|
35
|
-
onGenerated: (result: GeneratedSeoMetadata) => void;
|
|
36
|
-
/** Site title, when cheaply available, so the model can shape a title that suffixes well. */
|
|
37
|
-
siteTitle?: string | null;
|
|
38
|
-
/** The page's own title — the strongest single hint about what the page is. */
|
|
39
|
-
title?: string | null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Guard against a mistake that would otherwise be silent: if the route ever answers 200
|
|
44
|
-
* with a partial body, writing `undefined` into a controlled input flips it to
|
|
45
|
-
* uncontrolled and React warns once, at runtime, in a place far from the cause. Coercing
|
|
46
|
-
* here means a malformed response degrades to empty strings the operator can see.
|
|
47
|
-
*/
|
|
48
|
-
function readString(payload: Record<string, unknown>, key: string): string {
|
|
49
|
-
const value = payload[key];
|
|
50
|
-
return typeof value === 'string' ? value : '';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export default function GenerateMetaButton({
|
|
54
|
-
content,
|
|
55
|
-
focusKeyword,
|
|
56
|
-
locale,
|
|
57
|
-
onGenerated,
|
|
58
|
-
siteTitle,
|
|
59
|
-
title,
|
|
60
|
-
}: GenerateMetaButtonProps) {
|
|
61
|
-
const [isGenerating, setIsGenerating] = useState(false);
|
|
62
|
-
|
|
63
|
-
const trimmedContent = content.trim();
|
|
64
|
-
const canGenerate = trimmedContent.length > 0 && !isGenerating;
|
|
65
|
-
|
|
66
|
-
const handleGenerate = async () => {
|
|
67
|
-
if (!canGenerate) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setIsGenerating(true);
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const response = await fetch('/api/ai/seo/metadata', {
|
|
75
|
-
// Optional fields are omitted rather than sent as null: the contract types them
|
|
76
|
-
// as `string | undefined`, and a null would be a different, likely rejected, value.
|
|
77
|
-
body: JSON.stringify({
|
|
78
|
-
content: trimmedContent,
|
|
79
|
-
...(focusKeyword?.trim() ? { focusKeyword: focusKeyword.trim() } : {}),
|
|
80
|
-
...(locale?.trim() ? { locale: locale.trim() } : {}),
|
|
81
|
-
...(siteTitle?.trim() ? { siteTitle: siteTitle.trim() } : {}),
|
|
82
|
-
...(title?.trim() ? { title: title.trim() } : {}),
|
|
83
|
-
}),
|
|
84
|
-
headers: buildCortexAiRequestHeaders(),
|
|
85
|
-
method: 'POST',
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
89
|
-
|
|
90
|
-
if (!response.ok || !payload) {
|
|
91
|
-
// Every error shape from these routes is `{ error: string }` at 400/403/500, so a
|
|
92
|
-
// missing `error` means something upstream of the route failed (a proxy, a crash
|
|
93
|
-
// before the handler) and a generic message is the honest thing to show.
|
|
94
|
-
const message =
|
|
95
|
-
payload && typeof payload['error'] === 'string'
|
|
96
|
-
? (payload['error'] as string)
|
|
97
|
-
: 'Cortex AI could not generate metadata.';
|
|
98
|
-
throw new Error(message);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
onGenerated({
|
|
102
|
-
metaDescription: readString(payload, 'metaDescription'),
|
|
103
|
-
metaTitle: readString(payload, 'metaTitle'),
|
|
104
|
-
ogDescription: readString(payload, 'ogDescription'),
|
|
105
|
-
ogTitle: readString(payload, 'ogTitle'),
|
|
106
|
-
});
|
|
107
|
-
toast.success('Cortex AI drafted your SEO metadata.');
|
|
108
|
-
} catch (error) {
|
|
109
|
-
toast.error(error instanceof Error ? error.message : 'Cortex AI could not generate metadata.');
|
|
110
|
-
} finally {
|
|
111
|
-
setIsGenerating(false);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Button
|
|
117
|
-
className="h-7 gap-1.5 px-2 text-[11px]"
|
|
118
|
-
disabled={!canGenerate}
|
|
119
|
-
onClick={handleGenerate}
|
|
120
|
-
size="sm"
|
|
121
|
-
title={
|
|
122
|
-
trimmedContent
|
|
123
|
-
? 'Draft the meta title and description from this page’s content'
|
|
124
|
-
: 'Add some content to this page first — Cortex AI needs something to summarize'
|
|
125
|
-
}
|
|
126
|
-
type="button"
|
|
127
|
-
variant="outline"
|
|
128
|
-
>
|
|
129
|
-
{isGenerating ? (
|
|
130
|
-
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
|
|
131
|
-
) : (
|
|
132
|
-
<Sparkles aria-hidden="true" className="h-3.5 w-3.5 text-amber-500" />
|
|
133
|
-
)}
|
|
134
|
-
{isGenerating ? 'Generating…' : 'Generate Meta Data with AI'}
|
|
135
|
-
</Button>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Loader2, Sparkles } from 'lucide-react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import { Button } from '@nextblock-cms/ui';
|
|
7
|
+
|
|
8
|
+
import { buildCortexAiRequestHeaders } from '../../lib/cortex-ai/sandbox-headers';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The four strings `/api/ai/seo/metadata` writes back.
|
|
12
|
+
*
|
|
13
|
+
* Note what is NOT here: an image. The Open Graph image on a page or post is derived from
|
|
14
|
+
* `feature_image_id`, and nothing in this flow changes that. The model writes copy; the
|
|
15
|
+
* operator picks the picture.
|
|
16
|
+
*/
|
|
17
|
+
export interface GeneratedSeoMetadata {
|
|
18
|
+
metaDescription: string;
|
|
19
|
+
metaTitle: string;
|
|
20
|
+
ogDescription: string;
|
|
21
|
+
ogTitle: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface GenerateMetaButtonProps {
|
|
25
|
+
/**
|
|
26
|
+
* Body prose the model reads in order to write about the page. The caller flattens its
|
|
27
|
+
* blocks; an empty string disables the button, because a model asked to summarize
|
|
28
|
+
* nothing invents something, and invented metadata is worse than none.
|
|
29
|
+
*/
|
|
30
|
+
content: string;
|
|
31
|
+
/** Optional term the copy should be built around, when the caller knows one. */
|
|
32
|
+
focusKeyword?: string | null;
|
|
33
|
+
/** Language code (`en`, `fr`, …) so the copy comes back in the content's language. */
|
|
34
|
+
locale?: string | null;
|
|
35
|
+
onGenerated: (result: GeneratedSeoMetadata) => void;
|
|
36
|
+
/** Site title, when cheaply available, so the model can shape a title that suffixes well. */
|
|
37
|
+
siteTitle?: string | null;
|
|
38
|
+
/** The page's own title — the strongest single hint about what the page is. */
|
|
39
|
+
title?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Guard against a mistake that would otherwise be silent: if the route ever answers 200
|
|
44
|
+
* with a partial body, writing `undefined` into a controlled input flips it to
|
|
45
|
+
* uncontrolled and React warns once, at runtime, in a place far from the cause. Coercing
|
|
46
|
+
* here means a malformed response degrades to empty strings the operator can see.
|
|
47
|
+
*/
|
|
48
|
+
function readString(payload: Record<string, unknown>, key: string): string {
|
|
49
|
+
const value = payload[key];
|
|
50
|
+
return typeof value === 'string' ? value : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function GenerateMetaButton({
|
|
54
|
+
content,
|
|
55
|
+
focusKeyword,
|
|
56
|
+
locale,
|
|
57
|
+
onGenerated,
|
|
58
|
+
siteTitle,
|
|
59
|
+
title,
|
|
60
|
+
}: GenerateMetaButtonProps) {
|
|
61
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
62
|
+
|
|
63
|
+
const trimmedContent = content.trim();
|
|
64
|
+
const canGenerate = trimmedContent.length > 0 && !isGenerating;
|
|
65
|
+
|
|
66
|
+
const handleGenerate = async () => {
|
|
67
|
+
if (!canGenerate) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setIsGenerating(true);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await fetch('/api/ai/seo/metadata', {
|
|
75
|
+
// Optional fields are omitted rather than sent as null: the contract types them
|
|
76
|
+
// as `string | undefined`, and a null would be a different, likely rejected, value.
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
content: trimmedContent,
|
|
79
|
+
...(focusKeyword?.trim() ? { focusKeyword: focusKeyword.trim() } : {}),
|
|
80
|
+
...(locale?.trim() ? { locale: locale.trim() } : {}),
|
|
81
|
+
...(siteTitle?.trim() ? { siteTitle: siteTitle.trim() } : {}),
|
|
82
|
+
...(title?.trim() ? { title: title.trim() } : {}),
|
|
83
|
+
}),
|
|
84
|
+
headers: buildCortexAiRequestHeaders(),
|
|
85
|
+
method: 'POST',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
|
89
|
+
|
|
90
|
+
if (!response.ok || !payload) {
|
|
91
|
+
// Every error shape from these routes is `{ error: string }` at 400/403/500, so a
|
|
92
|
+
// missing `error` means something upstream of the route failed (a proxy, a crash
|
|
93
|
+
// before the handler) and a generic message is the honest thing to show.
|
|
94
|
+
const message =
|
|
95
|
+
payload && typeof payload['error'] === 'string'
|
|
96
|
+
? (payload['error'] as string)
|
|
97
|
+
: 'Cortex AI could not generate metadata.';
|
|
98
|
+
throw new Error(message);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onGenerated({
|
|
102
|
+
metaDescription: readString(payload, 'metaDescription'),
|
|
103
|
+
metaTitle: readString(payload, 'metaTitle'),
|
|
104
|
+
ogDescription: readString(payload, 'ogDescription'),
|
|
105
|
+
ogTitle: readString(payload, 'ogTitle'),
|
|
106
|
+
});
|
|
107
|
+
toast.success('Cortex AI drafted your SEO metadata.');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
toast.error(error instanceof Error ? error.message : 'Cortex AI could not generate metadata.');
|
|
110
|
+
} finally {
|
|
111
|
+
setIsGenerating(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Button
|
|
117
|
+
className="h-7 gap-1.5 px-2 text-[11px]"
|
|
118
|
+
disabled={!canGenerate}
|
|
119
|
+
onClick={handleGenerate}
|
|
120
|
+
size="sm"
|
|
121
|
+
title={
|
|
122
|
+
trimmedContent
|
|
123
|
+
? 'Draft the meta title and description from this page’s content'
|
|
124
|
+
: 'Add some content to this page first — Cortex AI needs something to summarize'
|
|
125
|
+
}
|
|
126
|
+
type="button"
|
|
127
|
+
variant="outline"
|
|
128
|
+
>
|
|
129
|
+
{isGenerating ? (
|
|
130
|
+
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
|
|
131
|
+
) : (
|
|
132
|
+
<Sparkles aria-hidden="true" className="h-3.5 w-3.5 text-amber-500" />
|
|
133
|
+
)}
|
|
134
|
+
{isGenerating ? 'Generating…' : 'Generate Meta Data with AI'}
|
|
135
|
+
</Button>
|
|
136
|
+
);
|
|
137
|
+
}
|