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
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { Share2 } from 'lucide-react';
|
|
5
|
-
import {
|
|
6
|
-
Button,
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogContent,
|
|
9
|
-
DialogDescription,
|
|
10
|
-
DialogHeader,
|
|
11
|
-
DialogTitle,
|
|
12
|
-
DialogTrigger,
|
|
13
|
-
} from '@nextblock-cms/ui';
|
|
14
|
-
import SocialPreview from './SocialPreview';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The share-card rehearsal, moved behind a button.
|
|
18
|
-
*
|
|
19
|
-
* WHY A MODAL. `SocialPreview` renders three platform tabs above a 1.91:1 card that is up
|
|
20
|
-
* to 520px wide, so inline it costs roughly four hundred vertical pixels in the middle of
|
|
21
|
-
* a page/post settings form that is already long enough to scroll. That is a poor trade for
|
|
22
|
-
* something an author consults once while wording a description and then never looks at
|
|
23
|
-
* again for the rest of the session. Behind a trigger it costs one line, and the moment it
|
|
24
|
-
* IS wanted it gets the whole viewport instead of a squeezed column.
|
|
25
|
-
*
|
|
26
|
-
* WHY THE PRESENTATIONAL COMPONENT STAYS UNTOUCHED. `SocialPreview` knows how three feeds
|
|
27
|
-
* clip a headline and nothing else — no open state, no trigger, no dialog. Keeping that
|
|
28
|
-
* split means the same preview can still be dropped inline anywhere it earns the space
|
|
29
|
-
* (a future site-wide SEO screen, a publish-confirmation step) without having to be
|
|
30
|
-
* unwrapped from a modal first. This component owns the disclosure; that one owns the
|
|
31
|
-
* drawing.
|
|
32
|
-
*
|
|
33
|
-
* WHY IT STAYS LIVE WHILE OPEN. The props below are the caller's live form state, so every
|
|
34
|
-
* keystroke in the meta fields behind the dialog repaints the card — the same behaviour the
|
|
35
|
-
* inline version had. The dialog is a React portal, not a snapshot, so nothing is frozen at
|
|
36
|
-
* open time and there is no "reopen to refresh" trap.
|
|
37
|
-
*
|
|
38
|
-
* NOTHING HERE PERSISTS. This component holds exactly one piece of state — whether the
|
|
39
|
-
* modal is open — and it is read by nobody but Radix. It contributes no form field, no
|
|
40
|
-
* FormData entry, and no input to the caller's autosave diff.
|
|
41
|
-
*/
|
|
42
|
-
interface SocialPreviewDialogProps {
|
|
43
|
-
description: string;
|
|
44
|
-
imageUrl?: string | null;
|
|
45
|
-
siteName?: string | null;
|
|
46
|
-
title: string;
|
|
47
|
-
/** Optional override for the trigger label, for surfaces that word it differently. */
|
|
48
|
-
triggerLabel?: string;
|
|
49
|
-
url: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default function SocialPreviewDialog({
|
|
53
|
-
description,
|
|
54
|
-
imageUrl,
|
|
55
|
-
siteName,
|
|
56
|
-
title,
|
|
57
|
-
triggerLabel = 'Preview share card',
|
|
58
|
-
url,
|
|
59
|
-
}: SocialPreviewDialogProps) {
|
|
60
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<Dialog onOpenChange={setIsOpen} open={isOpen}>
|
|
64
|
-
{/* `asChild` so the trigger is the house `Button` rather than a bare Radix button, and
|
|
65
|
-
`type="button"` explicitly: this renders inside the page/post `<form>`, where a
|
|
66
|
-
button that inherits the default `submit` type would save the record instead of
|
|
67
|
-
opening a preview. The visible text is the accessible name, and Radix leaves the
|
|
68
|
-
element a real focusable button, so keyboard and pointer reach it identically. */}
|
|
69
|
-
<DialogTrigger asChild>
|
|
70
|
-
<Button className="h-7 px-2 text-[11px]" size="sm" type="button" variant="outline">
|
|
71
|
-
<Share2 aria-hidden="true" className="mr-1.5 h-3.5 w-3.5" />
|
|
72
|
-
{triggerLabel}
|
|
73
|
-
</Button>
|
|
74
|
-
</DialogTrigger>
|
|
75
|
-
|
|
76
|
-
{/* The card is tall and the platform switcher sits above it, so the panel is capped at
|
|
77
|
-
85% of the viewport and the BODY scrolls rather than the dialog growing past the
|
|
78
|
-
screen edge — on a laptop in a browser with devtools open, an uncapped panel would
|
|
79
|
-
push its own close button out of reach. `overflow-hidden` on the shell keeps the
|
|
80
|
-
rounded corners from being cut by the scrolling child. */}
|
|
81
|
-
<DialogContent className="flex max-h-[85vh] flex-col gap-0 overflow-hidden sm:max-w-[600px]">
|
|
82
|
-
{/* `pr-8` reserves room for the close affordance the dialog draws at top-right. */}
|
|
83
|
-
<DialogHeader className="flex-shrink-0 pb-4 pr-8">
|
|
84
|
-
<DialogTitle>Share preview</DialogTitle>
|
|
85
|
-
<DialogDescription>
|
|
86
|
-
How this link is likely to appear when it is shared. Edit the meta title and
|
|
87
|
-
description behind this dialog and the card updates as you type.
|
|
88
|
-
</DialogDescription>
|
|
89
|
-
</DialogHeader>
|
|
90
|
-
|
|
91
|
-
<div className="flex-grow overflow-y-auto py-1">
|
|
92
|
-
<SocialPreview
|
|
93
|
-
description={description}
|
|
94
|
-
imageUrl={imageUrl}
|
|
95
|
-
siteName={siteName}
|
|
96
|
-
title={title}
|
|
97
|
-
url={url}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{/* Stated once here rather than repeated on each tab: the numbers in the preview are
|
|
102
|
-
tuned to observed rendering, and every platform reserves the right to reflow. */}
|
|
103
|
-
<p className="flex-shrink-0 pt-3 text-[11px] leading-snug text-muted-foreground">
|
|
104
|
-
Truncation is approximate — each platform reflows link previews on its own terms —
|
|
105
|
-
so treat a card that clips here as a prompt to shorten the copy, not a guarantee.
|
|
106
|
-
</p>
|
|
107
|
-
</DialogContent>
|
|
108
|
-
</Dialog>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Share2 } from 'lucide-react';
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from '@nextblock-cms/ui';
|
|
14
|
+
import SocialPreview from './SocialPreview';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The share-card rehearsal, moved behind a button.
|
|
18
|
+
*
|
|
19
|
+
* WHY A MODAL. `SocialPreview` renders three platform tabs above a 1.91:1 card that is up
|
|
20
|
+
* to 520px wide, so inline it costs roughly four hundred vertical pixels in the middle of
|
|
21
|
+
* a page/post settings form that is already long enough to scroll. That is a poor trade for
|
|
22
|
+
* something an author consults once while wording a description and then never looks at
|
|
23
|
+
* again for the rest of the session. Behind a trigger it costs one line, and the moment it
|
|
24
|
+
* IS wanted it gets the whole viewport instead of a squeezed column.
|
|
25
|
+
*
|
|
26
|
+
* WHY THE PRESENTATIONAL COMPONENT STAYS UNTOUCHED. `SocialPreview` knows how three feeds
|
|
27
|
+
* clip a headline and nothing else — no open state, no trigger, no dialog. Keeping that
|
|
28
|
+
* split means the same preview can still be dropped inline anywhere it earns the space
|
|
29
|
+
* (a future site-wide SEO screen, a publish-confirmation step) without having to be
|
|
30
|
+
* unwrapped from a modal first. This component owns the disclosure; that one owns the
|
|
31
|
+
* drawing.
|
|
32
|
+
*
|
|
33
|
+
* WHY IT STAYS LIVE WHILE OPEN. The props below are the caller's live form state, so every
|
|
34
|
+
* keystroke in the meta fields behind the dialog repaints the card — the same behaviour the
|
|
35
|
+
* inline version had. The dialog is a React portal, not a snapshot, so nothing is frozen at
|
|
36
|
+
* open time and there is no "reopen to refresh" trap.
|
|
37
|
+
*
|
|
38
|
+
* NOTHING HERE PERSISTS. This component holds exactly one piece of state — whether the
|
|
39
|
+
* modal is open — and it is read by nobody but Radix. It contributes no form field, no
|
|
40
|
+
* FormData entry, and no input to the caller's autosave diff.
|
|
41
|
+
*/
|
|
42
|
+
interface SocialPreviewDialogProps {
|
|
43
|
+
description: string;
|
|
44
|
+
imageUrl?: string | null;
|
|
45
|
+
siteName?: string | null;
|
|
46
|
+
title: string;
|
|
47
|
+
/** Optional override for the trigger label, for surfaces that word it differently. */
|
|
48
|
+
triggerLabel?: string;
|
|
49
|
+
url: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function SocialPreviewDialog({
|
|
53
|
+
description,
|
|
54
|
+
imageUrl,
|
|
55
|
+
siteName,
|
|
56
|
+
title,
|
|
57
|
+
triggerLabel = 'Preview share card',
|
|
58
|
+
url,
|
|
59
|
+
}: SocialPreviewDialogProps) {
|
|
60
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog onOpenChange={setIsOpen} open={isOpen}>
|
|
64
|
+
{/* `asChild` so the trigger is the house `Button` rather than a bare Radix button, and
|
|
65
|
+
`type="button"` explicitly: this renders inside the page/post `<form>`, where a
|
|
66
|
+
button that inherits the default `submit` type would save the record instead of
|
|
67
|
+
opening a preview. The visible text is the accessible name, and Radix leaves the
|
|
68
|
+
element a real focusable button, so keyboard and pointer reach it identically. */}
|
|
69
|
+
<DialogTrigger asChild>
|
|
70
|
+
<Button className="h-7 px-2 text-[11px]" size="sm" type="button" variant="outline">
|
|
71
|
+
<Share2 aria-hidden="true" className="mr-1.5 h-3.5 w-3.5" />
|
|
72
|
+
{triggerLabel}
|
|
73
|
+
</Button>
|
|
74
|
+
</DialogTrigger>
|
|
75
|
+
|
|
76
|
+
{/* The card is tall and the platform switcher sits above it, so the panel is capped at
|
|
77
|
+
85% of the viewport and the BODY scrolls rather than the dialog growing past the
|
|
78
|
+
screen edge — on a laptop in a browser with devtools open, an uncapped panel would
|
|
79
|
+
push its own close button out of reach. `overflow-hidden` on the shell keeps the
|
|
80
|
+
rounded corners from being cut by the scrolling child. */}
|
|
81
|
+
<DialogContent className="flex max-h-[85vh] flex-col gap-0 overflow-hidden sm:max-w-[600px]">
|
|
82
|
+
{/* `pr-8` reserves room for the close affordance the dialog draws at top-right. */}
|
|
83
|
+
<DialogHeader className="flex-shrink-0 pb-4 pr-8">
|
|
84
|
+
<DialogTitle>Share preview</DialogTitle>
|
|
85
|
+
<DialogDescription>
|
|
86
|
+
How this link is likely to appear when it is shared. Edit the meta title and
|
|
87
|
+
description behind this dialog and the card updates as you type.
|
|
88
|
+
</DialogDescription>
|
|
89
|
+
</DialogHeader>
|
|
90
|
+
|
|
91
|
+
<div className="flex-grow overflow-y-auto py-1">
|
|
92
|
+
<SocialPreview
|
|
93
|
+
description={description}
|
|
94
|
+
imageUrl={imageUrl}
|
|
95
|
+
siteName={siteName}
|
|
96
|
+
title={title}
|
|
97
|
+
url={url}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Stated once here rather than repeated on each tab: the numbers in the preview are
|
|
102
|
+
tuned to observed rendering, and every platform reserves the right to reflow. */}
|
|
103
|
+
<p className="flex-shrink-0 pt-3 text-[11px] leading-snug text-muted-foreground">
|
|
104
|
+
Truncation is approximate — each platform reflows link previews on its own terms —
|
|
105
|
+
so treat a card that clips here as a prompt to shorten the copy, not a guarantee.
|
|
106
|
+
</p>
|
|
107
|
+
</DialogContent>
|
|
108
|
+
</Dialog>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@nextblock-cms/ui";
|
|
4
|
-
import { type ComponentProps } from "react";
|
|
5
|
-
import { useFormStatus } from "react-dom";
|
|
6
|
-
|
|
7
|
-
type Props = ComponentProps<typeof Button> & {
|
|
8
|
-
pendingText?: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function SubmitButton({
|
|
12
|
-
children,
|
|
13
|
-
pendingText = "Submitting...",
|
|
14
|
-
...props
|
|
15
|
-
}: Props) {
|
|
16
|
-
const { pending } = useFormStatus();
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<Button type="submit" aria-disabled={pending} {...props}>
|
|
20
|
-
{pending ? pendingText : children}
|
|
21
|
-
</Button>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
import { type ComponentProps } from "react";
|
|
5
|
+
import { useFormStatus } from "react-dom";
|
|
6
|
+
|
|
7
|
+
type Props = ComponentProps<typeof Button> & {
|
|
8
|
+
pendingText?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SubmitButton({
|
|
12
|
+
children,
|
|
13
|
+
pendingText = "Submitting...",
|
|
14
|
+
...props
|
|
15
|
+
}: Props) {
|
|
16
|
+
const { pending } = useFormStatus();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Button type="submit" aria-disabled={pending} {...props}>
|
|
20
|
+
{pending ? pendingText : children}
|
|
21
|
+
</Button>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Aperture,
|
|
5
|
-
Atom,
|
|
6
|
-
Blend,
|
|
7
|
-
Brush,
|
|
8
|
-
Cloud,
|
|
9
|
-
Contrast,
|
|
10
|
-
Droplet,
|
|
11
|
-
Feather,
|
|
12
|
-
Flame,
|
|
13
|
-
Gem,
|
|
14
|
-
Ghost,
|
|
15
|
-
Leaf,
|
|
16
|
-
Moon,
|
|
17
|
-
Mountain,
|
|
18
|
-
Palette,
|
|
19
|
-
Rocket,
|
|
20
|
-
Snowflake,
|
|
21
|
-
Sparkles,
|
|
22
|
-
Star,
|
|
23
|
-
Sun,
|
|
24
|
-
Sunrise,
|
|
25
|
-
Sunset,
|
|
26
|
-
Waves,
|
|
27
|
-
Zap,
|
|
28
|
-
type LucideIcon,
|
|
29
|
-
} from "lucide-react";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Curated icon set offered to themes.
|
|
33
|
-
*
|
|
34
|
-
* A fixed map rather than a dynamic lucide lookup: the icon name comes from the
|
|
35
|
-
* database, and importing lucide dynamically by string would either pull the
|
|
36
|
-
* whole icon set into the bundle or break tree-shaking.
|
|
37
|
-
*/
|
|
38
|
-
export const THEME_ICONS: Record<string, LucideIcon> = {
|
|
39
|
-
Sun,
|
|
40
|
-
Moon,
|
|
41
|
-
Zap,
|
|
42
|
-
Sparkles,
|
|
43
|
-
Palette,
|
|
44
|
-
Flame,
|
|
45
|
-
Droplet,
|
|
46
|
-
Leaf,
|
|
47
|
-
Star,
|
|
48
|
-
Cloud,
|
|
49
|
-
Snowflake,
|
|
50
|
-
Waves,
|
|
51
|
-
Mountain,
|
|
52
|
-
Sunrise,
|
|
53
|
-
Sunset,
|
|
54
|
-
Contrast,
|
|
55
|
-
Blend,
|
|
56
|
-
Brush,
|
|
57
|
-
Aperture,
|
|
58
|
-
Atom,
|
|
59
|
-
Feather,
|
|
60
|
-
Gem,
|
|
61
|
-
Ghost,
|
|
62
|
-
Rocket,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const THEME_ICON_NAMES = Object.keys(THEME_ICONS);
|
|
66
|
-
|
|
67
|
-
export function ThemeIcon({
|
|
68
|
-
name,
|
|
69
|
-
size = 16,
|
|
70
|
-
className,
|
|
71
|
-
}: {
|
|
72
|
-
name: string;
|
|
73
|
-
size?: number;
|
|
74
|
-
className?: string;
|
|
75
|
-
}) {
|
|
76
|
-
const Icon = THEME_ICONS[name] ?? Palette;
|
|
77
|
-
return <Icon size={size} className={className} />;
|
|
78
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Aperture,
|
|
5
|
+
Atom,
|
|
6
|
+
Blend,
|
|
7
|
+
Brush,
|
|
8
|
+
Cloud,
|
|
9
|
+
Contrast,
|
|
10
|
+
Droplet,
|
|
11
|
+
Feather,
|
|
12
|
+
Flame,
|
|
13
|
+
Gem,
|
|
14
|
+
Ghost,
|
|
15
|
+
Leaf,
|
|
16
|
+
Moon,
|
|
17
|
+
Mountain,
|
|
18
|
+
Palette,
|
|
19
|
+
Rocket,
|
|
20
|
+
Snowflake,
|
|
21
|
+
Sparkles,
|
|
22
|
+
Star,
|
|
23
|
+
Sun,
|
|
24
|
+
Sunrise,
|
|
25
|
+
Sunset,
|
|
26
|
+
Waves,
|
|
27
|
+
Zap,
|
|
28
|
+
type LucideIcon,
|
|
29
|
+
} from "lucide-react";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Curated icon set offered to themes.
|
|
33
|
+
*
|
|
34
|
+
* A fixed map rather than a dynamic lucide lookup: the icon name comes from the
|
|
35
|
+
* database, and importing lucide dynamically by string would either pull the
|
|
36
|
+
* whole icon set into the bundle or break tree-shaking.
|
|
37
|
+
*/
|
|
38
|
+
export const THEME_ICONS: Record<string, LucideIcon> = {
|
|
39
|
+
Sun,
|
|
40
|
+
Moon,
|
|
41
|
+
Zap,
|
|
42
|
+
Sparkles,
|
|
43
|
+
Palette,
|
|
44
|
+
Flame,
|
|
45
|
+
Droplet,
|
|
46
|
+
Leaf,
|
|
47
|
+
Star,
|
|
48
|
+
Cloud,
|
|
49
|
+
Snowflake,
|
|
50
|
+
Waves,
|
|
51
|
+
Mountain,
|
|
52
|
+
Sunrise,
|
|
53
|
+
Sunset,
|
|
54
|
+
Contrast,
|
|
55
|
+
Blend,
|
|
56
|
+
Brush,
|
|
57
|
+
Aperture,
|
|
58
|
+
Atom,
|
|
59
|
+
Feather,
|
|
60
|
+
Gem,
|
|
61
|
+
Ghost,
|
|
62
|
+
Rocket,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const THEME_ICON_NAMES = Object.keys(THEME_ICONS);
|
|
66
|
+
|
|
67
|
+
export function ThemeIcon({
|
|
68
|
+
name,
|
|
69
|
+
size = 16,
|
|
70
|
+
className,
|
|
71
|
+
}: {
|
|
72
|
+
name: string;
|
|
73
|
+
size?: number;
|
|
74
|
+
className?: string;
|
|
75
|
+
}) {
|
|
76
|
+
const Icon = THEME_ICONS[name] ?? Palette;
|
|
77
|
+
return <Icon size={size} className={className} />;
|
|
78
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Button } from "@nextblock-cms/ui";
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuRadioGroup,
|
|
8
|
-
DropdownMenuRadioItem,
|
|
9
|
-
DropdownMenuTrigger,
|
|
10
|
-
} from "@nextblock-cms/ui";
|
|
11
|
-
import { Laptop } from "lucide-react";
|
|
12
|
-
import { useTheme } from "next-themes";
|
|
13
|
-
import { useEffect, useState } from "react";
|
|
14
|
-
import { useTranslations } from "@nextblock-cms/utils";
|
|
15
|
-
import { useThemeCatalog } from "../context/ThemeCatalogContext";
|
|
16
|
-
import { ThemeIcon } from "./theme-icon";
|
|
17
|
-
|
|
18
|
-
const ICON_SIZE = 16;
|
|
19
|
-
|
|
20
|
-
const ThemeSwitcher = () => {
|
|
21
|
-
const [mounted, setMounted] = useState(false);
|
|
22
|
-
const { theme, setTheme } = useTheme();
|
|
23
|
-
const { t } = useTranslations();
|
|
24
|
-
const themes = useThemeCatalog();
|
|
25
|
-
|
|
26
|
-
// useEffect only runs on the client, so now we can safely show the UI
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
setMounted(true);
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
if (!mounted) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const active = themes.find((entry) => entry.slug === theme);
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Built-in slugs keep their existing translation keys so no copy is lost;
|
|
39
|
-
* admin-created themes fall back to the name stored on the row.
|
|
40
|
-
*/
|
|
41
|
-
const labelFor = (slug: string, name: string) => {
|
|
42
|
-
const builtin: Record<string, string> = {
|
|
43
|
-
light: "theme_light",
|
|
44
|
-
dark: "theme_dark",
|
|
45
|
-
vibrant: "theme_vibrant",
|
|
46
|
-
};
|
|
47
|
-
const key = builtin[slug];
|
|
48
|
-
if (!key) return name;
|
|
49
|
-
const translated = t(key);
|
|
50
|
-
return translated === key ? name : translated;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<DropdownMenu>
|
|
55
|
-
<DropdownMenuTrigger asChild>
|
|
56
|
-
<Button variant="ghost" size={"sm"} aria-label={t('theme_switcher')}>
|
|
57
|
-
{active ? (
|
|
58
|
-
<ThemeIcon name={active.icon} size={ICON_SIZE} className="text-muted-foreground" />
|
|
59
|
-
) : (
|
|
60
|
-
<Laptop size={ICON_SIZE} className="text-muted-foreground" />
|
|
61
|
-
)}
|
|
62
|
-
</Button>
|
|
63
|
-
</DropdownMenuTrigger>
|
|
64
|
-
<DropdownMenuContent className="w-content" align="start">
|
|
65
|
-
<DropdownMenuRadioGroup
|
|
66
|
-
value={theme}
|
|
67
|
-
onValueChange={(e) => setTheme(e)}
|
|
68
|
-
>
|
|
69
|
-
{themes.map((entry) => (
|
|
70
|
-
<DropdownMenuRadioItem key={entry.slug} className="flex gap-2" value={entry.slug}>
|
|
71
|
-
<ThemeIcon name={entry.icon} size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
72
|
-
<span>{labelFor(entry.slug, entry.name)}</span>
|
|
73
|
-
</DropdownMenuRadioItem>
|
|
74
|
-
))}
|
|
75
|
-
<DropdownMenuRadioItem className="flex gap-2" value="system">
|
|
76
|
-
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
77
|
-
<span>{t('theme_system')}</span>
|
|
78
|
-
</DropdownMenuRadioItem>
|
|
79
|
-
</DropdownMenuRadioGroup>
|
|
80
|
-
</DropdownMenuContent>
|
|
81
|
-
</DropdownMenu>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export { ThemeSwitcher };
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@nextblock-cms/ui";
|
|
11
|
+
import { Laptop } from "lucide-react";
|
|
12
|
+
import { useTheme } from "next-themes";
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
import { useTranslations } from "@nextblock-cms/utils";
|
|
15
|
+
import { useThemeCatalog } from "../context/ThemeCatalogContext";
|
|
16
|
+
import { ThemeIcon } from "./theme-icon";
|
|
17
|
+
|
|
18
|
+
const ICON_SIZE = 16;
|
|
19
|
+
|
|
20
|
+
const ThemeSwitcher = () => {
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
|
+
const { theme, setTheme } = useTheme();
|
|
23
|
+
const { t } = useTranslations();
|
|
24
|
+
const themes = useThemeCatalog();
|
|
25
|
+
|
|
26
|
+
// useEffect only runs on the client, so now we can safely show the UI
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setMounted(true);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
if (!mounted) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const active = themes.find((entry) => entry.slug === theme);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Built-in slugs keep their existing translation keys so no copy is lost;
|
|
39
|
+
* admin-created themes fall back to the name stored on the row.
|
|
40
|
+
*/
|
|
41
|
+
const labelFor = (slug: string, name: string) => {
|
|
42
|
+
const builtin: Record<string, string> = {
|
|
43
|
+
light: "theme_light",
|
|
44
|
+
dark: "theme_dark",
|
|
45
|
+
vibrant: "theme_vibrant",
|
|
46
|
+
};
|
|
47
|
+
const key = builtin[slug];
|
|
48
|
+
if (!key) return name;
|
|
49
|
+
const translated = t(key);
|
|
50
|
+
return translated === key ? name : translated;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<DropdownMenu>
|
|
55
|
+
<DropdownMenuTrigger asChild>
|
|
56
|
+
<Button variant="ghost" size={"sm"} aria-label={t('theme_switcher')}>
|
|
57
|
+
{active ? (
|
|
58
|
+
<ThemeIcon name={active.icon} size={ICON_SIZE} className="text-muted-foreground" />
|
|
59
|
+
) : (
|
|
60
|
+
<Laptop size={ICON_SIZE} className="text-muted-foreground" />
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
<DropdownMenuContent className="w-content" align="start">
|
|
65
|
+
<DropdownMenuRadioGroup
|
|
66
|
+
value={theme}
|
|
67
|
+
onValueChange={(e) => setTheme(e)}
|
|
68
|
+
>
|
|
69
|
+
{themes.map((entry) => (
|
|
70
|
+
<DropdownMenuRadioItem key={entry.slug} className="flex gap-2" value={entry.slug}>
|
|
71
|
+
<ThemeIcon name={entry.icon} size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
72
|
+
<span>{labelFor(entry.slug, entry.name)}</span>
|
|
73
|
+
</DropdownMenuRadioItem>
|
|
74
|
+
))}
|
|
75
|
+
<DropdownMenuRadioItem className="flex gap-2" value="system">
|
|
76
|
+
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
77
|
+
<span>{t('theme_system')}</span>
|
|
78
|
+
</DropdownMenuRadioItem>
|
|
79
|
+
</DropdownMenuRadioGroup>
|
|
80
|
+
</DropdownMenuContent>
|
|
81
|
+
</DropdownMenu>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export { ThemeSwitcher };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// context/AuthContext.tsx
|
|
2
|
-
'use client';
|
|
3
|
-
|
|
4
|
-
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
|
5
|
-
import type { ReactNode } from 'react';
|
|
6
|
-
import type { User, SupabaseClient } from '@supabase/supabase-js';
|
|
7
|
-
import type { Database } from '@nextblock-cms/db';
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
import type { User, SupabaseClient } from '@supabase/supabase-js';
|
|
7
|
+
import type { Database } from '@nextblock-cms/db';
|
|
8
8
|
|
|
9
9
|
type Profile = Database['public']['Tables']['profiles']['Row'];
|
|
10
10
|
type UserRole = Database['public']['Enums']['user_role'];
|
|
@@ -28,17 +28,17 @@ interface AuthContextType {
|
|
|
28
28
|
|
|
29
29
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
30
30
|
|
|
31
|
-
export const AuthProvider = ({ children, serverUser, serverProfile }: AuthProviderProps) => {
|
|
32
|
-
const [user, setUser] = useState<User | null>(serverUser);
|
|
33
|
-
const [profile, setProfile] = useState<Profile | null>(serverProfile);
|
|
34
|
-
const [role, setRole] = useState<UserRole | null>(serverProfile?.role ?? null);
|
|
35
|
-
const [isLoading] = useState(false);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
setUser(serverUser);
|
|
39
|
-
setProfile(serverProfile);
|
|
40
|
-
setRole(serverProfile?.role ?? null);
|
|
41
|
-
}, [serverUser, serverProfile]);
|
|
31
|
+
export const AuthProvider = ({ children, serverUser, serverProfile }: AuthProviderProps) => {
|
|
32
|
+
const [user, setUser] = useState<User | null>(serverUser);
|
|
33
|
+
const [profile, setProfile] = useState<Profile | null>(serverProfile);
|
|
34
|
+
const [role, setRole] = useState<UserRole | null>(serverProfile?.role ?? null);
|
|
35
|
+
const [isLoading] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setUser(serverUser);
|
|
39
|
+
setProfile(serverProfile);
|
|
40
|
+
setRole(serverProfile?.role ?? null);
|
|
41
|
+
}, [serverUser, serverProfile]);
|
|
42
42
|
|
|
43
43
|
const isAdmin = role === 'ADMIN';
|
|
44
44
|
const isWriter = role === 'WRITER';
|
|
@@ -49,11 +49,11 @@ export const AuthProvider = ({ children, serverUser, serverProfile }: AuthProvid
|
|
|
49
49
|
profile,
|
|
50
50
|
role,
|
|
51
51
|
isLoading,
|
|
52
|
-
isAdmin,
|
|
53
|
-
isWriter,
|
|
54
|
-
isUserRole,
|
|
55
|
-
supabase: null,
|
|
56
|
-
}), [user, profile, role, isLoading, isAdmin, isWriter, isUserRole]);
|
|
52
|
+
isAdmin,
|
|
53
|
+
isWriter,
|
|
54
|
+
isUserRole,
|
|
55
|
+
supabase: null,
|
|
56
|
+
}), [user, profile, role, isLoading, isAdmin, isWriter, isUserRole]);
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
59
|
<AuthContext.Provider value={value}>
|
|
@@ -68,4 +68,4 @@ export const useAuth = () => {
|
|
|
68
68
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
69
69
|
}
|
|
70
70
|
return context;
|
|
71
|
-
};
|
|
71
|
+
};
|