create-nextblock 0.14.1 → 0.14.4
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 +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/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 +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- 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/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5881 -5623
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
- 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/cms/CmsClientLayout.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- 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/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
- 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/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- 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 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- 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/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- 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 +49 -0
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- 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 +16 -4
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- 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/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +77 -82
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +279 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- 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/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
- package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -10
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/next-env.d.ts +2 -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/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
|
@@ -4,11 +4,27 @@
|
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { Label } from "@nextblock-cms/ui";
|
|
6
6
|
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { ColorField } from "@nextblock-cms/ui";
|
|
7
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@nextblock-cms/ui";
|
|
8
9
|
import { HeadingBlockContent } from '../../../../lib/blocks/blockRegistry';
|
|
10
|
+
import { TEXT_COLOR_TOKEN_OPTIONS } from '../../../../lib/blocks/blockColors';
|
|
9
11
|
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
/** Handy starting points that are not in the theme — brand-neutral and legible. */
|
|
14
|
+
const HEADING_COLOR_PRESETS = [
|
|
15
|
+
'#0F172A',
|
|
16
|
+
'#334155',
|
|
17
|
+
'#DC2626',
|
|
18
|
+
'#EA580C',
|
|
19
|
+
'#CA8A04',
|
|
20
|
+
'#16A34A',
|
|
21
|
+
'#0891B2',
|
|
22
|
+
'#2563EB',
|
|
23
|
+
'#7C3AED',
|
|
24
|
+
'#DB2777',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export default function HeadingBlockEditor({ content, onChange, sectionBackground }: BlockEditorProps<Partial<HeadingBlockContent>>) {
|
|
12
28
|
const idPrefix = React.useId();
|
|
13
29
|
|
|
14
30
|
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -21,24 +37,26 @@ export default function HeadingBlockEditor({ content, onChange }: BlockEditorPro
|
|
|
21
37
|
|
|
22
38
|
const textAlignOptions = ['left', 'center', 'right', 'justify'] as const;
|
|
23
39
|
|
|
24
|
-
const textColorOptions = [
|
|
25
|
-
{ value: 'primary', label: 'Primary', swatchClass: 'bg-primary' },
|
|
26
|
-
{ value: 'secondary', label: 'Secondary', swatchClass: 'bg-secondary' },
|
|
27
|
-
{ value: 'accent', label: 'Accent', swatchClass: 'bg-accent' },
|
|
28
|
-
{ value: 'muted', label: 'Muted', swatchClass: 'bg-muted-foreground' }, // Using muted-foreground for swatch as text-muted is for text
|
|
29
|
-
{ value: 'destructive', label: 'Destructive', swatchClass: 'bg-destructive' },
|
|
30
|
-
{ value: 'background', label: 'Background', swatchClass: 'bg-background' },
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
40
|
const handleTextAlignChange = (value: string) => {
|
|
34
41
|
onChange({ ...content, textAlign: value as HeadingBlockContent['textAlign'] });
|
|
35
42
|
};
|
|
36
43
|
|
|
37
|
-
const handleTextColorChange = (value: string) => {
|
|
38
|
-
|
|
39
|
-
onChange({ ...content, textColor: newTextColor });
|
|
44
|
+
const handleTextColorChange = (value: string | undefined) => {
|
|
45
|
+
onChange({ ...content, textColor: value as HeadingBlockContent['textColor'] });
|
|
40
46
|
};
|
|
41
47
|
|
|
48
|
+
// Measure contrast against the real section backdrop when there is one, so the
|
|
49
|
+
// warning reflects what the visitor will actually see.
|
|
50
|
+
const contrastAgainst = React.useMemo(() => {
|
|
51
|
+
if (sectionBackground?.type === 'solid') return sectionBackground.solid_color;
|
|
52
|
+
if (sectionBackground?.type === 'theme' && sectionBackground.theme) {
|
|
53
|
+
return `hsl(var(--${sectionBackground.theme}))`;
|
|
54
|
+
}
|
|
55
|
+
if (!sectionBackground || sectionBackground.type === 'none') return 'hsl(var(--background))';
|
|
56
|
+
// Gradients and images have no single backdrop colour to measure against.
|
|
57
|
+
return undefined;
|
|
58
|
+
}, [sectionBackground]);
|
|
59
|
+
|
|
42
60
|
return (
|
|
43
61
|
<div className="space-y-3 p-3 border-t mt-2">
|
|
44
62
|
<div>
|
|
@@ -85,27 +103,20 @@ export default function HeadingBlockEditor({ content, onChange }: BlockEditorPro
|
|
|
85
103
|
</SelectContent>
|
|
86
104
|
</Select>
|
|
87
105
|
</div>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{color.label}
|
|
103
|
-
</div>
|
|
104
|
-
</SelectItem>
|
|
105
|
-
))}
|
|
106
|
-
</SelectContent>
|
|
107
|
-
</Select>
|
|
108
|
-
</div>
|
|
106
|
+
<ColorField
|
|
107
|
+
id={`heading-text-color-${idPrefix}`}
|
|
108
|
+
label="Text Color"
|
|
109
|
+
description="Theme colours follow light/dark mode. Custom colours stay fixed."
|
|
110
|
+
value={content.textColor}
|
|
111
|
+
onChange={handleTextColorChange}
|
|
112
|
+
tokens={TEXT_COLOR_TOKEN_OPTIONS}
|
|
113
|
+
presets={HEADING_COLOR_PRESETS}
|
|
114
|
+
contrastAgainst={contrastAgainst}
|
|
115
|
+
largeText
|
|
116
|
+
enableAlpha
|
|
117
|
+
clearLabel="Inherit from theme"
|
|
118
|
+
placeholder="Inherit from theme"
|
|
119
|
+
/>
|
|
109
120
|
</div>
|
|
110
121
|
);
|
|
111
122
|
}
|
|
@@ -157,7 +157,9 @@ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps
|
|
|
157
157
|
width={content.width}
|
|
158
158
|
height={content.height}
|
|
159
159
|
className="rounded-md object-contain"
|
|
160
|
-
|
|
160
|
+
// width/height auto so the CSS max-height scales both axes and
|
|
161
|
+
// keeps the intrinsic aspect ratio (next/image warns otherwise)
|
|
162
|
+
style={{ width: 'auto', height: 'auto', maxWidth: '100%', maxHeight: '200px' }}
|
|
161
163
|
placeholder={content.blur_data_url ? "blur" : "empty"}
|
|
162
164
|
blurDataURL={content.blur_data_url || undefined}
|
|
163
165
|
/>
|
|
@@ -175,7 +177,7 @@ export default function ImageBlockEditor({ content, onChange }: BlockEditorProps
|
|
|
175
177
|
width={300}
|
|
176
178
|
height={200}
|
|
177
179
|
className="rounded-md object-contain max-h-40 block"
|
|
178
|
-
style={{
|
|
180
|
+
style={{ width: 'auto', height: 'auto', maxWidth: '100%' }}
|
|
179
181
|
/>
|
|
180
182
|
<Button
|
|
181
183
|
type="button" variant="destructive" size="icon"
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// app/cms/blocks/editors/TextBlockEditor.tsx
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
|
-
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import dynamic from 'next/dynamic';
|
|
6
|
-
import type { Extensions } from '@tiptap/core';
|
|
7
|
-
import MediaPickerDialog from '../../media/components/MediaPickerDialog';
|
|
4
|
+
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import dynamic from 'next/dynamic';
|
|
6
|
+
import type { Extensions } from '@tiptap/core';
|
|
7
|
+
import MediaPickerDialog from '../../media/components/MediaPickerDialog';
|
|
8
8
|
import { Label } from '@nextblock-cms/ui';
|
|
9
9
|
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
10
|
-
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
11
|
-
import { useCortexAiActive } from '../../components/CortexAiActiveContext';
|
|
12
|
-
import {
|
|
13
|
-
createDynamicCustomBlockExtensions,
|
|
14
|
-
type DynamicCustomBlockEditorDefinition,
|
|
15
|
-
} from '../../../../lib/editor/dynamic-extensions';
|
|
10
|
+
import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
|
|
11
|
+
import { useCortexAiActive } from '../../components/CortexAiActiveContext';
|
|
12
|
+
import {
|
|
13
|
+
createDynamicCustomBlockExtensions,
|
|
14
|
+
type DynamicCustomBlockEditorDefinition,
|
|
15
|
+
} from '../../../../lib/editor/dynamic-extensions';
|
|
16
16
|
|
|
17
17
|
// Props expected by NotionEditor
|
|
18
18
|
type NotionEditorProps = {
|
|
19
19
|
content: string;
|
|
20
20
|
onChange: (html: string) => void;
|
|
21
|
-
openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
|
|
22
|
-
className?: string;
|
|
23
|
-
showAiPrompt?: boolean;
|
|
24
|
-
dynamicExtensions?: Extensions;
|
|
25
|
-
};
|
|
21
|
+
openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
|
|
22
|
+
className?: string;
|
|
23
|
+
showAiPrompt?: boolean;
|
|
24
|
+
dynamicExtensions?: Extensions;
|
|
25
|
+
};
|
|
26
26
|
|
|
27
27
|
// Use the alias that resolves in your repo; if you mapped @nextblock-cms/editor, swap it here.
|
|
28
28
|
const NotionEditor = dynamic<NotionEditorProps>(
|
|
@@ -40,91 +40,91 @@ export default function TextBlockEditor({
|
|
|
40
40
|
className,
|
|
41
41
|
}: BlockEditorProps<Partial<TextBlockContent>>) {
|
|
42
42
|
const labelId = useId();
|
|
43
|
-
const [pickerOpen, setPickerOpen] = useState(false);
|
|
44
|
-
const [dynamicDefinitions, setDynamicDefinitions] = useState<DynamicCustomBlockEditorDefinition[]>([]);
|
|
45
|
-
const [dynamicDefinitionsLoaded, setDynamicDefinitionsLoaded] = useState(false);
|
|
46
|
-
const isCortexAiActive = useCortexAiActive();
|
|
47
|
-
const resolverRef = useRef<null | ((v: any) => void)>(null);
|
|
43
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
44
|
+
const [dynamicDefinitions, setDynamicDefinitions] = useState<DynamicCustomBlockEditorDefinition[]>([]);
|
|
45
|
+
const [dynamicDefinitionsLoaded, setDynamicDefinitionsLoaded] = useState(false);
|
|
46
|
+
const isCortexAiActive = useCortexAiActive();
|
|
47
|
+
const resolverRef = useRef<null | ((v: any) => void)>(null);
|
|
48
48
|
const openImagePicker = useCallback(() => {
|
|
49
49
|
setPickerOpen(true);
|
|
50
50
|
return new Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>((resolve) => {
|
|
51
51
|
resolverRef.current = resolve;
|
|
52
|
-
});
|
|
53
|
-
}, []);
|
|
54
|
-
const dynamicExtensions = useMemo(
|
|
55
|
-
() => createDynamicCustomBlockExtensions(dynamicDefinitions),
|
|
56
|
-
[dynamicDefinitions]
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
let isActive = true;
|
|
61
|
-
|
|
62
|
-
async function loadDynamicDefinitions() {
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch('/api/custom-blocks/editor-definitions', {
|
|
65
|
-
cache: 'no-store',
|
|
66
|
-
method: 'GET',
|
|
67
|
-
});
|
|
68
|
-
const payload = (await response.json()) as {
|
|
69
|
-
definitions?: DynamicCustomBlockEditorDefinition[];
|
|
70
|
-
error?: string;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
if (!isActive) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!response.ok) {
|
|
78
|
-
console.error(
|
|
79
|
-
'[TextBlockEditor] Failed to load custom block editor definitions:',
|
|
80
|
-
payload.error
|
|
81
|
-
);
|
|
82
|
-
setDynamicDefinitions([]);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
setDynamicDefinitions(payload.definitions ?? []);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (isActive) {
|
|
89
|
-
console.error('[TextBlockEditor] Failed to load custom block editor definitions:', error);
|
|
90
|
-
setDynamicDefinitions([]);
|
|
91
|
-
}
|
|
92
|
-
} finally {
|
|
93
|
-
if (isActive) {
|
|
94
|
-
setDynamicDefinitionsLoaded(true);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
void loadDynamicDefinitions();
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
isActive = false;
|
|
103
|
-
};
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
return (
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
const dynamicExtensions = useMemo(
|
|
55
|
+
() => createDynamicCustomBlockExtensions(dynamicDefinitions),
|
|
56
|
+
[dynamicDefinitions]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let isActive = true;
|
|
61
|
+
|
|
62
|
+
async function loadDynamicDefinitions() {
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch('/api/custom-blocks/editor-definitions', {
|
|
65
|
+
cache: 'no-store',
|
|
66
|
+
method: 'GET',
|
|
67
|
+
});
|
|
68
|
+
const payload = (await response.json()) as {
|
|
69
|
+
definitions?: DynamicCustomBlockEditorDefinition[];
|
|
70
|
+
error?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (!isActive) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
console.error(
|
|
79
|
+
'[TextBlockEditor] Failed to load custom block editor definitions:',
|
|
80
|
+
payload.error
|
|
81
|
+
);
|
|
82
|
+
setDynamicDefinitions([]);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setDynamicDefinitions(payload.definitions ?? []);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isActive) {
|
|
89
|
+
console.error('[TextBlockEditor] Failed to load custom block editor definitions:', error);
|
|
90
|
+
setDynamicDefinitions([]);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
if (isActive) {
|
|
94
|
+
setDynamicDefinitionsLoaded(true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void loadDynamicDefinitions();
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
isActive = false;
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
107
|
<div className="h-full flex flex-col">
|
|
108
108
|
<Label htmlFor={labelId} className="sr-only">
|
|
109
109
|
Text Content
|
|
110
110
|
</Label>
|
|
111
111
|
|
|
112
112
|
<div id={labelId} role="group" aria-labelledby={labelId} className="flex-1 min-h-0 flex flex-col">
|
|
113
|
-
{dynamicDefinitionsLoaded ? (
|
|
114
|
-
<NotionEditor
|
|
115
|
-
key={dynamicDefinitions.map((definition) => definition.id).join('|') || 'static'}
|
|
116
|
-
content={content?.html_content ?? ''}
|
|
117
|
-
dynamicExtensions={dynamicExtensions}
|
|
118
|
-
onChange={(html) => onChange({ html_content: html })}
|
|
119
|
-
openImagePicker={openImagePicker}
|
|
120
|
-
className={className}
|
|
121
|
-
showAiPrompt={isCortexAiActive}
|
|
122
|
-
/>
|
|
123
|
-
) : (
|
|
124
|
-
<div className="flex min-h-[500px] flex-1 items-center justify-center rounded-lg border bg-background text-sm text-muted-foreground">
|
|
125
|
-
Loading editor...
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
113
|
+
{dynamicDefinitionsLoaded ? (
|
|
114
|
+
<NotionEditor
|
|
115
|
+
key={dynamicDefinitions.map((definition) => definition.id).join('|') || 'static'}
|
|
116
|
+
content={content?.html_content ?? ''}
|
|
117
|
+
dynamicExtensions={dynamicExtensions}
|
|
118
|
+
onChange={(html) => onChange({ html_content: html })}
|
|
119
|
+
openImagePicker={openImagePicker}
|
|
120
|
+
className={className}
|
|
121
|
+
showAiPrompt={isCortexAiActive}
|
|
122
|
+
/>
|
|
123
|
+
) : (
|
|
124
|
+
<div className="flex min-h-[500px] flex-1 items-center justify-center rounded-lg border bg-background text-sm text-muted-foreground">
|
|
125
|
+
Loading editor...
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
128
|
|
|
129
129
|
{/* Hidden controlled MediaPickerDialog for image selection */}
|
|
130
130
|
<div className="sr-only" aria-hidden>
|
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Check, ExternalLink, Github, Loader2 } from 'lucide-react';
|
|
6
|
-
import { Button } from '@nextblock-cms/ui';
|
|
7
|
-
import { startGithubConnect, pollGithubConnect } from './github-connect-actions';
|
|
8
|
-
|
|
9
|
-
type Phase = 'idle' | 'starting' | 'awaiting' | 'installed' | 'error';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* One-click "Connect GitHub" via the OAuth device flow. On authorization, the server
|
|
13
|
-
* installs the upstream-sync workflow into the repo (the file Vercel's clone strips).
|
|
14
|
-
* No PAT, no env config — the public client id is baked into the app.
|
|
15
|
-
*/
|
|
16
|
-
export default function ConnectGitHubButton() {
|
|
17
|
-
const router = useRouter();
|
|
18
|
-
const [phase, setPhase] = useState<Phase>('idle');
|
|
19
|
-
const [userCode, setUserCode] = useState('');
|
|
20
|
-
const [verificationUri, setVerificationUri] = useState('https://github.com/login/device');
|
|
21
|
-
const [error, setError] = useState('');
|
|
22
|
-
|
|
23
|
-
// Cancel any in-flight polling when the component unmounts.
|
|
24
|
-
const activeRef = useRef(false);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
activeRef.current = true;
|
|
27
|
-
return () => {
|
|
28
|
-
activeRef.current = false;
|
|
29
|
-
};
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
const poll = useCallback(async (intervalMs: number) => {
|
|
33
|
-
if (!activeRef.current) return;
|
|
34
|
-
const result = await pollGithubConnect();
|
|
35
|
-
if (!activeRef.current) return;
|
|
36
|
-
|
|
37
|
-
if (result.status === 'installed') {
|
|
38
|
-
setPhase('installed');
|
|
39
|
-
// Re-render the dashboard so the onboarding step picks up the now-active workflow
|
|
40
|
-
// (the step then flips to done and this control is replaced).
|
|
41
|
-
router.refresh();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (result.status === 'error') {
|
|
45
|
-
setError(result.error);
|
|
46
|
-
setPhase('error');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
// pending — back off a little on slow_down, then poll again.
|
|
50
|
-
const next = result.slowDown ? intervalMs + 5000 : intervalMs;
|
|
51
|
-
setTimeout(() => void poll(next), next);
|
|
52
|
-
}, [router]);
|
|
53
|
-
|
|
54
|
-
const connect = useCallback(async () => {
|
|
55
|
-
setError('');
|
|
56
|
-
setPhase('starting');
|
|
57
|
-
const res = await startGithubConnect();
|
|
58
|
-
if (!activeRef.current) return;
|
|
59
|
-
if (!res.ok || !res.userCode) {
|
|
60
|
-
setError(res.error ?? 'Could not start GitHub connect.');
|
|
61
|
-
setPhase('error');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
setUserCode(res.userCode);
|
|
65
|
-
if (res.verificationUri) setVerificationUri(res.verificationUri);
|
|
66
|
-
setPhase('awaiting');
|
|
67
|
-
const intervalMs = Math.max((res.interval ?? 5) + 1, 5) * 1000;
|
|
68
|
-
setTimeout(() => void poll(intervalMs), intervalMs);
|
|
69
|
-
}, [poll]);
|
|
70
|
-
|
|
71
|
-
if (phase === 'installed') {
|
|
72
|
-
return (
|
|
73
|
-
<div className="flex items-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
|
74
|
-
<Check className="h-4 w-4" />
|
|
75
|
-
Connected — workflow installed
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (phase === 'awaiting') {
|
|
81
|
-
return (
|
|
82
|
-
<div className="flex flex-col items-end gap-1.5 text-right">
|
|
83
|
-
<Button asChild size="sm" variant="outline">
|
|
84
|
-
<a href={verificationUri} target="_blank" rel="noopener noreferrer">
|
|
85
|
-
Authorize on GitHub
|
|
86
|
-
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
|
87
|
-
</a>
|
|
88
|
-
</Button>
|
|
89
|
-
<p className="text-xs text-muted-foreground">
|
|
90
|
-
Enter code{' '}
|
|
91
|
-
<span className="font-mono font-semibold tracking-wider text-foreground">{userCode}</span>
|
|
92
|
-
</p>
|
|
93
|
-
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
94
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
95
|
-
Waiting for authorization…
|
|
96
|
-
</p>
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div className="flex flex-col items-end gap-1">
|
|
103
|
-
<Button
|
|
104
|
-
size="sm"
|
|
105
|
-
variant="outline"
|
|
106
|
-
onClick={() => void connect()}
|
|
107
|
-
disabled={phase === 'starting'}
|
|
108
|
-
className="shrink-0"
|
|
109
|
-
>
|
|
110
|
-
{phase === 'starting' ? (
|
|
111
|
-
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
112
|
-
) : (
|
|
113
|
-
<Github className="mr-1 h-3.5 w-3.5" />
|
|
114
|
-
)}
|
|
115
|
-
{phase === 'starting' ? 'Starting…' : 'Connect GitHub'}
|
|
116
|
-
</Button>
|
|
117
|
-
{phase === 'error' && (
|
|
118
|
-
<p className="max-w-[16rem] text-right text-xs text-red-600 dark:text-red-400">{error}</p>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Check, ExternalLink, Github, Loader2 } from 'lucide-react';
|
|
6
|
+
import { Button } from '@nextblock-cms/ui';
|
|
7
|
+
import { startGithubConnect, pollGithubConnect } from './github-connect-actions';
|
|
8
|
+
|
|
9
|
+
type Phase = 'idle' | 'starting' | 'awaiting' | 'installed' | 'error';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One-click "Connect GitHub" via the OAuth device flow. On authorization, the server
|
|
13
|
+
* installs the upstream-sync workflow into the repo (the file Vercel's clone strips).
|
|
14
|
+
* No PAT, no env config — the public client id is baked into the app.
|
|
15
|
+
*/
|
|
16
|
+
export default function ConnectGitHubButton() {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const [phase, setPhase] = useState<Phase>('idle');
|
|
19
|
+
const [userCode, setUserCode] = useState('');
|
|
20
|
+
const [verificationUri, setVerificationUri] = useState('https://github.com/login/device');
|
|
21
|
+
const [error, setError] = useState('');
|
|
22
|
+
|
|
23
|
+
// Cancel any in-flight polling when the component unmounts.
|
|
24
|
+
const activeRef = useRef(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
activeRef.current = true;
|
|
27
|
+
return () => {
|
|
28
|
+
activeRef.current = false;
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const poll = useCallback(async (intervalMs: number) => {
|
|
33
|
+
if (!activeRef.current) return;
|
|
34
|
+
const result = await pollGithubConnect();
|
|
35
|
+
if (!activeRef.current) return;
|
|
36
|
+
|
|
37
|
+
if (result.status === 'installed') {
|
|
38
|
+
setPhase('installed');
|
|
39
|
+
// Re-render the dashboard so the onboarding step picks up the now-active workflow
|
|
40
|
+
// (the step then flips to done and this control is replaced).
|
|
41
|
+
router.refresh();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (result.status === 'error') {
|
|
45
|
+
setError(result.error);
|
|
46
|
+
setPhase('error');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// pending — back off a little on slow_down, then poll again.
|
|
50
|
+
const next = result.slowDown ? intervalMs + 5000 : intervalMs;
|
|
51
|
+
setTimeout(() => void poll(next), next);
|
|
52
|
+
}, [router]);
|
|
53
|
+
|
|
54
|
+
const connect = useCallback(async () => {
|
|
55
|
+
setError('');
|
|
56
|
+
setPhase('starting');
|
|
57
|
+
const res = await startGithubConnect();
|
|
58
|
+
if (!activeRef.current) return;
|
|
59
|
+
if (!res.ok || !res.userCode) {
|
|
60
|
+
setError(res.error ?? 'Could not start GitHub connect.');
|
|
61
|
+
setPhase('error');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setUserCode(res.userCode);
|
|
65
|
+
if (res.verificationUri) setVerificationUri(res.verificationUri);
|
|
66
|
+
setPhase('awaiting');
|
|
67
|
+
const intervalMs = Math.max((res.interval ?? 5) + 1, 5) * 1000;
|
|
68
|
+
setTimeout(() => void poll(intervalMs), intervalMs);
|
|
69
|
+
}, [poll]);
|
|
70
|
+
|
|
71
|
+
if (phase === 'installed') {
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex items-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400">
|
|
74
|
+
<Check className="h-4 w-4" />
|
|
75
|
+
Connected — workflow installed
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (phase === 'awaiting') {
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col items-end gap-1.5 text-right">
|
|
83
|
+
<Button asChild size="sm" variant="outline">
|
|
84
|
+
<a href={verificationUri} target="_blank" rel="noopener noreferrer">
|
|
85
|
+
Authorize on GitHub
|
|
86
|
+
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
|
87
|
+
</a>
|
|
88
|
+
</Button>
|
|
89
|
+
<p className="text-xs text-muted-foreground">
|
|
90
|
+
Enter code{' '}
|
|
91
|
+
<span className="font-mono font-semibold tracking-wider text-foreground">{userCode}</span>
|
|
92
|
+
</p>
|
|
93
|
+
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
94
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
95
|
+
Waiting for authorization…
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex flex-col items-end gap-1">
|
|
103
|
+
<Button
|
|
104
|
+
size="sm"
|
|
105
|
+
variant="outline"
|
|
106
|
+
onClick={() => void connect()}
|
|
107
|
+
disabled={phase === 'starting'}
|
|
108
|
+
className="shrink-0"
|
|
109
|
+
>
|
|
110
|
+
{phase === 'starting' ? (
|
|
111
|
+
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
112
|
+
) : (
|
|
113
|
+
<Github className="mr-1 h-3.5 w-3.5" />
|
|
114
|
+
)}
|
|
115
|
+
{phase === 'starting' ? 'Starting…' : 'Connect GitHub'}
|
|
116
|
+
</Button>
|
|
117
|
+
{phase === 'error' && (
|
|
118
|
+
<p className="max-w-[16rem] text-right text-xs text-red-600 dark:text-red-400">{error}</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|