create-nextblock 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// app/cms/blocks/editors/HeadingBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Label } from "@nextblock-cms/ui";
|
|
6
|
+
import { Input } from "@nextblock-cms/ui";
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@nextblock-cms/ui";
|
|
8
|
+
import type { HeadingBlockContent } from "@/lib/blocks/blockRegistry";
|
|
9
|
+
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
10
|
+
|
|
11
|
+
export default function HeadingBlockEditor({ content, onChange }: BlockEditorProps<Partial<HeadingBlockContent>>) {
|
|
12
|
+
const idPrefix = React.useId();
|
|
13
|
+
|
|
14
|
+
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
onChange({ ...content, text_content: event.target.value });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleLevelChange = (value: string) => {
|
|
19
|
+
onChange({ ...content, level: parseInt(value, 10) as HeadingBlockContent['level'] });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const textAlignOptions = ['left', 'center', 'right', 'justify'] as const;
|
|
23
|
+
|
|
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
|
+
const handleTextAlignChange = (value: string) => {
|
|
34
|
+
onChange({ ...content, textAlign: value as HeadingBlockContent['textAlign'] });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleTextColorChange = (value: string) => {
|
|
38
|
+
const newTextColor = value === "" ? undefined : value as HeadingBlockContent['textColor'];
|
|
39
|
+
onChange({ ...content, textColor: newTextColor });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="space-y-3 p-3 border-t mt-2">
|
|
44
|
+
<div>
|
|
45
|
+
<Label htmlFor={`heading-text-${idPrefix}`}>Heading Text</Label>
|
|
46
|
+
<Input
|
|
47
|
+
id={`heading-text-${idPrefix}`}
|
|
48
|
+
value={content.text_content || ""}
|
|
49
|
+
onChange={handleTextChange}
|
|
50
|
+
placeholder="Enter heading text"
|
|
51
|
+
className="mt-1"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<Label htmlFor={`heading-level-${idPrefix}`}>Level</Label>
|
|
56
|
+
<Select
|
|
57
|
+
value={content.level?.toString() || "2"}
|
|
58
|
+
onValueChange={handleLevelChange}
|
|
59
|
+
>
|
|
60
|
+
<SelectTrigger id={`heading-level-${idPrefix}`} className="mt-1">
|
|
61
|
+
<SelectValue placeholder="Select level" />
|
|
62
|
+
</SelectTrigger>
|
|
63
|
+
<SelectContent>
|
|
64
|
+
{[1, 2, 3, 4, 5, 6].map(level => (
|
|
65
|
+
<SelectItem key={level} value={level.toString()}>H{level}</SelectItem>
|
|
66
|
+
))}
|
|
67
|
+
</SelectContent>
|
|
68
|
+
</Select>
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<Label htmlFor={`heading-text-align-${idPrefix}`}>Text Alignment</Label>
|
|
72
|
+
<Select
|
|
73
|
+
value={content.textAlign || 'left'}
|
|
74
|
+
onValueChange={handleTextAlignChange}
|
|
75
|
+
>
|
|
76
|
+
<SelectTrigger id={`heading-text-align-${idPrefix}`} className="mt-1">
|
|
77
|
+
<SelectValue placeholder="Select alignment" />
|
|
78
|
+
</SelectTrigger>
|
|
79
|
+
<SelectContent>
|
|
80
|
+
{textAlignOptions.map(align => (
|
|
81
|
+
<SelectItem key={align} value={align}>
|
|
82
|
+
{align.charAt(0).toUpperCase() + align.slice(1)}
|
|
83
|
+
</SelectItem>
|
|
84
|
+
))}
|
|
85
|
+
</SelectContent>
|
|
86
|
+
</Select>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<Label htmlFor={`heading-text-color-${idPrefix}`}>Text Color</Label>
|
|
90
|
+
<Select
|
|
91
|
+
value={content.textColor || ""} // Use empty string if no color is selected initially
|
|
92
|
+
onValueChange={handleTextColorChange}
|
|
93
|
+
>
|
|
94
|
+
<SelectTrigger id={`heading-text-color-${idPrefix}`} className="mt-1">
|
|
95
|
+
<SelectValue placeholder="Select color (optional)" />
|
|
96
|
+
</SelectTrigger>
|
|
97
|
+
<SelectContent>
|
|
98
|
+
{textColorOptions.map(color => (
|
|
99
|
+
<SelectItem key={color.value} value={color.value}>
|
|
100
|
+
<div className="flex items-center">
|
|
101
|
+
<div className={`w-4 h-4 rounded-sm mr-2 border ${color.swatchClass}`}></div>
|
|
102
|
+
{color.label}
|
|
103
|
+
</div>
|
|
104
|
+
</SelectItem>
|
|
105
|
+
))}
|
|
106
|
+
</SelectContent>
|
|
107
|
+
</Select>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// app/cms/blocks/editors/ImageBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState } from 'react'; // Removed useTransition as it's not used here
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import { Label } from "@nextblock-cms/ui";
|
|
7
|
+
import { Input } from "@nextblock-cms/ui";
|
|
8
|
+
import { Button } from "@nextblock-cms/ui";
|
|
9
|
+
import type { Database } from "@nextblock-cms/db";
|
|
10
|
+
|
|
11
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
12
|
+
export type ImageBlockContent = {
|
|
13
|
+
media_id: string | null;
|
|
14
|
+
object_key: string | null;
|
|
15
|
+
alt_text: string | null;
|
|
16
|
+
caption: string | null;
|
|
17
|
+
width: number | null;
|
|
18
|
+
height: number | null;
|
|
19
|
+
blur_data_url: string | null;
|
|
20
|
+
};
|
|
21
|
+
import { ImageIcon, X as XIcon } from 'lucide-react';
|
|
22
|
+
import MediaPickerDialog from "@/app/cms/media/components/MediaPickerDialog"; // Import the upload form
|
|
23
|
+
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
24
|
+
|
|
25
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
26
|
+
|
|
27
|
+
export default function ImageBlockEditor({ content, onChange }: BlockEditorProps<Partial<ImageBlockContent>>) {
|
|
28
|
+
const [selectedMediaObjectKey, setSelectedMediaObjectKey] = useState<string | null | undefined>(content.object_key);
|
|
29
|
+
const [isLoadingMediaDetails] = useState(false); // For fetching details if only ID is present
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// Effect to fetch media details (like object_key) if only media_id is present in content
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const handleSelectMediaFromLibrary = (mediaItem: Media) => {
|
|
36
|
+
// Always reset alt to the new media's description (or derived from filename)
|
|
37
|
+
const deriveAltFromFilename = (name: string) => {
|
|
38
|
+
const lastDot = name.lastIndexOf('.');
|
|
39
|
+
const base = lastDot > 0 ? name.substring(0, lastDot) : name;
|
|
40
|
+
const spaced = base.replace(/[-+_\\]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
41
|
+
return spaced.replace(/\b\w+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
42
|
+
};
|
|
43
|
+
const newAlt = mediaItem.description && mediaItem.description.trim().length > 0
|
|
44
|
+
? mediaItem.description
|
|
45
|
+
: deriveAltFromFilename(mediaItem.file_name || 'Image');
|
|
46
|
+
|
|
47
|
+
setSelectedMediaObjectKey(mediaItem.object_key);
|
|
48
|
+
onChange({
|
|
49
|
+
media_id: mediaItem.id,
|
|
50
|
+
object_key: mediaItem.object_key,
|
|
51
|
+
alt_text: newAlt, // overwrite alt when image changes
|
|
52
|
+
caption: content.caption || "",
|
|
53
|
+
width: mediaItem.width,
|
|
54
|
+
height: mediaItem.height,
|
|
55
|
+
blur_data_url: mediaItem.blur_data_url,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleAltTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
60
|
+
onChange({
|
|
61
|
+
...content,
|
|
62
|
+
media_id: content.media_id || null,
|
|
63
|
+
object_key: selectedMediaObjectKey,
|
|
64
|
+
alt_text: event.target.value,
|
|
65
|
+
width: content.width,
|
|
66
|
+
height: content.height,
|
|
67
|
+
blur_data_url: content.blur_data_url
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleCaptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
72
|
+
onChange({
|
|
73
|
+
...content,
|
|
74
|
+
media_id: content.media_id || null,
|
|
75
|
+
object_key: selectedMediaObjectKey,
|
|
76
|
+
caption: event.target.value,
|
|
77
|
+
width: content.width,
|
|
78
|
+
height: content.height,
|
|
79
|
+
blur_data_url: content.blur_data_url
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleRemoveImage = () => {
|
|
84
|
+
setSelectedMediaObjectKey(null);
|
|
85
|
+
onChange({ media_id: null, object_key: null, alt_text: "", caption: "", width: null, height: null, blur_data_url: null });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const displayObjectKey = content.object_key || selectedMediaObjectKey;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-3 p-3 border-t mt-2">
|
|
92
|
+
<Label>Image</Label>
|
|
93
|
+
<div className="mt-1 p-3 border rounded-md bg-muted/30 min-h-[120px] flex flex-col items-center justify-center">
|
|
94
|
+
{isLoadingMediaDetails && <p>Loading image details...</p>}
|
|
95
|
+
{!isLoadingMediaDetails && displayObjectKey && typeof content.width === 'number' && typeof content.height === 'number' && content.width > 0 && content.height > 0 ? (
|
|
96
|
+
<div className="relative group inline-block" style={{ maxWidth: content.width, maxHeight: 200 }}> {/* Max height for editor preview consistency */}
|
|
97
|
+
<Image
|
|
98
|
+
src={`${R2_BASE_URL}/${displayObjectKey}`}
|
|
99
|
+
alt={content.alt_text || "Selected image"}
|
|
100
|
+
width={content.width}
|
|
101
|
+
height={content.height}
|
|
102
|
+
className="rounded-md object-contain" // Removed max-h-40, relying on width/height and parent max-height
|
|
103
|
+
style={{ maxHeight: '200px' }} // Ensure image does not exceed this height in preview
|
|
104
|
+
placeholder={content.blur_data_url ? "blur" : "empty"}
|
|
105
|
+
blurDataURL={content.blur_data_url || undefined}
|
|
106
|
+
/>
|
|
107
|
+
<Button
|
|
108
|
+
type="button" variant="destructive" size="icon"
|
|
109
|
+
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
|
|
110
|
+
onClick={handleRemoveImage} title="Remove Image"
|
|
111
|
+
> <XIcon className="h-3 w-3" /> </Button>
|
|
112
|
+
</div>
|
|
113
|
+
) : !isLoadingMediaDetails && displayObjectKey ? ( // Fallback if width/height are missing but key exists
|
|
114
|
+
<div className="relative group inline-block">
|
|
115
|
+
<Image
|
|
116
|
+
src={`${R2_BASE_URL}/${displayObjectKey}`}
|
|
117
|
+
alt={content.alt_text || "Selected image"}
|
|
118
|
+
width={300}
|
|
119
|
+
height={200}
|
|
120
|
+
className="rounded-md object-contain max-h-40 block"
|
|
121
|
+
style={{ maxHeight: '200px' }}
|
|
122
|
+
/>
|
|
123
|
+
<Button
|
|
124
|
+
type="button" variant="destructive" size="icon"
|
|
125
|
+
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
|
|
126
|
+
onClick={handleRemoveImage} title="Remove Image"
|
|
127
|
+
> <XIcon className="h-3 w-3" /> </Button>
|
|
128
|
+
<p className="text-xs text-orange-500 mt-1">Preview: Dimensions missing, using fallback.</p>
|
|
129
|
+
</div>
|
|
130
|
+
) : !isLoadingMediaDetails && content.media_id ? (
|
|
131
|
+
<p className="text-sm text-red-500">Image details (object_key or dimensions) missing for Media ID: {content.media_id}. Try re-selecting.</p>
|
|
132
|
+
) : (
|
|
133
|
+
<ImageIcon className="h-16 w-16 text-muted-foreground" />
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
<MediaPickerDialog triggerLabel={displayObjectKey ? "Change Image" : "Select from Library"} onSelect={handleSelectMediaFromLibrary} accept={(m)=>!!m.file_type?.startsWith("image/")} title="Select or Upload Image" />
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div>
|
|
140
|
+
<Label htmlFor={`image-alt-${content.media_id || 'new'}`}>Alt Text</Label>
|
|
141
|
+
<Input id={`image-alt-${content.media_id || 'new'}`} value={content.alt_text || ""} onChange={handleAltTextChange} className="mt-1" disabled={!displayObjectKey} />
|
|
142
|
+
</div>
|
|
143
|
+
<div>
|
|
144
|
+
<Label htmlFor={`image-caption-${content.media_id || 'new'}`}>Caption</Label>
|
|
145
|
+
<Input id={`image-caption-${content.media_id || 'new'}`} value={content.caption || ""} onChange={handleCaptionChange} className="mt-1" disabled={!displayObjectKey} />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// app/cms/blocks/editors/PostsGridBlockEditor.tsx
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
4
|
+
import { Input } from '@nextblock-cms/ui';
|
|
5
|
+
import { Label } from '@nextblock-cms/ui';
|
|
6
|
+
// import { useToast } from "@nextblock-cms/ui"; // Assuming you have a toast component - Removed for now
|
|
7
|
+
|
|
8
|
+
interface PostsGridBlockContent {
|
|
9
|
+
title?: string;
|
|
10
|
+
postsPerPage?: number;
|
|
11
|
+
columns?: number;
|
|
12
|
+
showPagination?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PostsGridBlockEditor: React.FC<BlockEditorProps<PostsGridBlockContent>> = ({ content, onChange }) => {
|
|
16
|
+
const [currentTitle, setCurrentTitle] = useState(content.title || 'Recent Posts');
|
|
17
|
+
const [currentPostsPerPage, setCurrentPostsPerPage] = useState(content.postsPerPage || 6);
|
|
18
|
+
const [currentColumns, setCurrentColumns] = useState(content.columns || 3);
|
|
19
|
+
const showPagination = content.showPagination === undefined ? true : content.showPagination;
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const newContentPayload = {
|
|
23
|
+
title: currentTitle,
|
|
24
|
+
postsPerPage: Number(currentPostsPerPage),
|
|
25
|
+
columns: Number(currentColumns),
|
|
26
|
+
showPagination: showPagination,
|
|
27
|
+
};
|
|
28
|
+
onChange(newContentPayload);
|
|
29
|
+
}, [currentTitle, currentPostsPerPage, currentColumns, showPagination, onChange]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-4 p-4 border rounded-md">
|
|
33
|
+
<h4 className="text-lg font-semibold">Posts Grid Block Editor</h4>
|
|
34
|
+
|
|
35
|
+
<div>
|
|
36
|
+
<Label htmlFor="posts-grid-title">Title</Label>
|
|
37
|
+
<Input
|
|
38
|
+
id="posts-grid-title"
|
|
39
|
+
value={currentTitle}
|
|
40
|
+
onChange={(e) => setCurrentTitle(e.target.value)}
|
|
41
|
+
placeholder="Enter title for the posts grid"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div>
|
|
46
|
+
<Label htmlFor="posts-grid-per-page">Posts Per Page</Label>
|
|
47
|
+
<Input
|
|
48
|
+
id="posts-grid-per-page"
|
|
49
|
+
type="number"
|
|
50
|
+
value={currentPostsPerPage}
|
|
51
|
+
onChange={(e) => setCurrentPostsPerPage(parseInt(e.target.value, 10))}
|
|
52
|
+
min="1"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div>
|
|
57
|
+
<Label htmlFor="posts-grid-columns">Columns</Label>
|
|
58
|
+
<Input
|
|
59
|
+
id="posts-grid-columns"
|
|
60
|
+
type="number"
|
|
61
|
+
value={currentColumns}
|
|
62
|
+
onChange={(e) => setCurrentColumns(parseInt(e.target.value, 10))}
|
|
63
|
+
min="1"
|
|
64
|
+
max="6" // Example max, adjust as needed
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<p className="text-sm">
|
|
69
|
+
<strong>Show Pagination:</strong> {showPagination ? 'Yes' : 'No'}
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<p className="text-xs text-muted-foreground pt-2">
|
|
73
|
+
Displays a grid of posts. Frontend rendering and further configuration options will be implemented in subsequent steps.
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default PostsGridBlockEditor;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// app/cms/blocks/editors/SectionBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useMemo } from "react";
|
|
5
|
+
import ColumnEditor from "../components/ColumnEditor";
|
|
6
|
+
import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
|
|
7
|
+
import {
|
|
8
|
+
getBlockDefinition,
|
|
9
|
+
} from "@/lib/blocks/blockRegistry";
|
|
10
|
+
import SectionConfigPanel from "../components/SectionConfigPanel";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// DND Kit imports for column block reordering
|
|
15
|
+
import {
|
|
16
|
+
DndContext,
|
|
17
|
+
closestCorners,
|
|
18
|
+
KeyboardSensor,
|
|
19
|
+
PointerSensor,
|
|
20
|
+
useSensor,
|
|
21
|
+
useSensors,
|
|
22
|
+
DragEndEvent,
|
|
23
|
+
DragStartEvent,
|
|
24
|
+
DragOverlay,
|
|
25
|
+
defaultDropAnimationSideEffects,
|
|
26
|
+
DropAnimation,
|
|
27
|
+
} from "@dnd-kit/core";
|
|
28
|
+
import {
|
|
29
|
+
SortableContext,
|
|
30
|
+
sortableKeyboardCoordinates,
|
|
31
|
+
verticalListSortingStrategy,
|
|
32
|
+
} from "@dnd-kit/sortable";
|
|
33
|
+
|
|
34
|
+
interface SectionBlockEditorProps {
|
|
35
|
+
content: Partial<SectionBlockContent>;
|
|
36
|
+
onChange: (newContent: SectionBlockContent) => void;
|
|
37
|
+
isConfigPanelOpen: boolean;
|
|
38
|
+
blockType: 'section' | 'hero';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function SectionBlockEditor({
|
|
42
|
+
content,
|
|
43
|
+
onChange,
|
|
44
|
+
isConfigPanelOpen,
|
|
45
|
+
blockType,
|
|
46
|
+
}: SectionBlockEditorProps) {
|
|
47
|
+
|
|
48
|
+
const processedContent = useMemo((): SectionBlockContent => {
|
|
49
|
+
const defaults: SectionBlockContent = {
|
|
50
|
+
container_type: "container",
|
|
51
|
+
background: { type: "none" },
|
|
52
|
+
responsive_columns: { mobile: 1, tablet: 2, desktop: 3 },
|
|
53
|
+
column_gap: "md",
|
|
54
|
+
padding: { top: "md", bottom: "md" },
|
|
55
|
+
column_blocks: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
container_type: content.container_type ?? defaults.container_type,
|
|
60
|
+
background: content.background ?? defaults.background,
|
|
61
|
+
responsive_columns:
|
|
62
|
+
content.responsive_columns ?? defaults.responsive_columns,
|
|
63
|
+
column_gap: content.column_gap ?? defaults.column_gap,
|
|
64
|
+
padding: content.padding ?? defaults.padding,
|
|
65
|
+
column_blocks: content.column_blocks ?? defaults.column_blocks,
|
|
66
|
+
};
|
|
67
|
+
}, [content]);
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
71
|
+
const [draggedBlock, setDraggedBlock] = useState<any>(null);
|
|
72
|
+
|
|
73
|
+
// DND sensors for cross-column dragging
|
|
74
|
+
const sensors = useSensors(
|
|
75
|
+
useSensor(PointerSensor, {
|
|
76
|
+
activationConstraint: {
|
|
77
|
+
distance: 8,
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
useSensor(KeyboardSensor, {
|
|
81
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleColumnBlocksChange = (
|
|
86
|
+
columnIndex: number,
|
|
87
|
+
newBlocks: SectionBlockContent["column_blocks"][0]
|
|
88
|
+
) => {
|
|
89
|
+
const newColumns = [...(processedContent.column_blocks || [])];
|
|
90
|
+
newColumns[columnIndex] = newBlocks;
|
|
91
|
+
onChange({ ...processedContent, column_blocks: newColumns });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Get blocks for a specific column from the 2D array
|
|
95
|
+
const getColumnBlocks = (columnIndex: number) => {
|
|
96
|
+
// With 2D array structure, directly return the column's blocks
|
|
97
|
+
return (processedContent.column_blocks || [])[columnIndex] || [];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Parse drag item ID to get column and block indices
|
|
101
|
+
const parseDragId = (id: string) => {
|
|
102
|
+
if (!id) return null;
|
|
103
|
+
const blockMatch = id.match(/^(hero|section)-column-(\d+)-block-(\d+)$/);
|
|
104
|
+
if (blockMatch) {
|
|
105
|
+
return {
|
|
106
|
+
type: "block",
|
|
107
|
+
blockType: blockMatch[1],
|
|
108
|
+
columnIndex: parseInt(blockMatch[2], 10),
|
|
109
|
+
blockIndex: parseInt(blockMatch[3], 10),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const droppableMatch = id.match(/^(hero|section)-column-droppable-(\d+)$/);
|
|
113
|
+
if (droppableMatch) {
|
|
114
|
+
return {
|
|
115
|
+
type: "column",
|
|
116
|
+
blockType: droppableMatch[1],
|
|
117
|
+
columnIndex: parseInt(droppableMatch[2], 10),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Handle drag start - store the dragged block for overlay
|
|
124
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
125
|
+
const { active } = event;
|
|
126
|
+
setActiveId(active.id.toString());
|
|
127
|
+
|
|
128
|
+
const parsed = parseDragId(active.id.toString());
|
|
129
|
+
if (
|
|
130
|
+
parsed &&
|
|
131
|
+
parsed.type === "block" &&
|
|
132
|
+
parsed.columnIndex !== undefined &&
|
|
133
|
+
parsed.blockIndex !== undefined
|
|
134
|
+
) {
|
|
135
|
+
const block = (processedContent.column_blocks || [])[parsed.columnIndex]?.[
|
|
136
|
+
parsed.blockIndex
|
|
137
|
+
];
|
|
138
|
+
setDraggedBlock(block);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Handle drag end - move blocks between columns
|
|
143
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
144
|
+
const { active, over } = event;
|
|
145
|
+
setActiveId(null);
|
|
146
|
+
setDraggedBlock(null);
|
|
147
|
+
|
|
148
|
+
if (!over || active.id === over.id) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const activeData = parseDragId(active.id.toString());
|
|
153
|
+
const overData = parseDragId(over.id.toString());
|
|
154
|
+
|
|
155
|
+
if (!activeData) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const newColumnBlocks = [...(processedContent.column_blocks || [])];
|
|
160
|
+
const sourceColumnIndex = activeData.columnIndex;
|
|
161
|
+
const sourceBlockIndex = activeData.blockIndex;
|
|
162
|
+
|
|
163
|
+
// Guard against invalid source data
|
|
164
|
+
if (sourceColumnIndex === undefined || sourceBlockIndex === undefined)
|
|
165
|
+
return;
|
|
166
|
+
|
|
167
|
+
const sourceColumn = newColumnBlocks[sourceColumnIndex];
|
|
168
|
+
if (!sourceColumn) return;
|
|
169
|
+
|
|
170
|
+
// Remove the block from the source column
|
|
171
|
+
const [movedBlock] = sourceColumn.splice(sourceBlockIndex, 1);
|
|
172
|
+
if (!movedBlock) return;
|
|
173
|
+
|
|
174
|
+
// Determine the target and insert the block
|
|
175
|
+
if (overData?.type === "block") {
|
|
176
|
+
// Scenario 1: Dropped onto another block
|
|
177
|
+
const targetColumnIndex = overData.columnIndex;
|
|
178
|
+
const targetBlockIndex = overData.blockIndex;
|
|
179
|
+
if (
|
|
180
|
+
newColumnBlocks[targetColumnIndex] &&
|
|
181
|
+
targetBlockIndex !== undefined
|
|
182
|
+
) {
|
|
183
|
+
newColumnBlocks[targetColumnIndex].splice(
|
|
184
|
+
targetBlockIndex,
|
|
185
|
+
0,
|
|
186
|
+
movedBlock
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} else if (overData?.type === "column") {
|
|
190
|
+
// Scenario 2: Dropped on an empty column's droppable area
|
|
191
|
+
const targetColumnIndex = overData.columnIndex;
|
|
192
|
+
if (newColumnBlocks[targetColumnIndex]) {
|
|
193
|
+
newColumnBlocks[targetColumnIndex].push(movedBlock);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Scenario 3: Invalid drop, return block to original position
|
|
197
|
+
sourceColumn.splice(sourceBlockIndex, 0, movedBlock);
|
|
198
|
+
return; // Exit without calling onChange
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Final state update
|
|
202
|
+
onChange({
|
|
203
|
+
...processedContent,
|
|
204
|
+
column_blocks: newColumnBlocks,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Custom drop animation for better visual feedback
|
|
209
|
+
const dropAnimation: DropAnimation = {
|
|
210
|
+
sideEffects: defaultDropAnimationSideEffects({
|
|
211
|
+
styles: {
|
|
212
|
+
active: {
|
|
213
|
+
opacity: "0.5",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<DndContext
|
|
221
|
+
sensors={sensors}
|
|
222
|
+
collisionDetection={closestCorners}
|
|
223
|
+
onDragStart={handleDragStart}
|
|
224
|
+
onDragEnd={handleDragEnd}
|
|
225
|
+
>
|
|
226
|
+
<div className="space-y-6 p-4 border-t mt-2">
|
|
227
|
+
{isConfigPanelOpen && (
|
|
228
|
+
<SectionConfigPanel
|
|
229
|
+
content={processedContent}
|
|
230
|
+
onChange={(newPartialContent) => {
|
|
231
|
+
onChange({ ...processedContent, ...newPartialContent });
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Column Content Management */}
|
|
237
|
+
<div className="space-y-4">
|
|
238
|
+
<div className="flex items-center justify-between">
|
|
239
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
240
|
+
Column Content
|
|
241
|
+
</h3>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<SortableContext
|
|
245
|
+
items={(processedContent.column_blocks || [])
|
|
246
|
+
.flatMap((columnBlocks, columnIndex) =>
|
|
247
|
+
columnBlocks.map(
|
|
248
|
+
(_, blockIndex) =>
|
|
249
|
+
`${blockType}-column-${columnIndex}-block-${blockIndex}`
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
.concat(
|
|
253
|
+
Array.from(
|
|
254
|
+
{ length: (processedContent.column_blocks || []).length },
|
|
255
|
+
(_, i) => `${blockType}-column-droppable-${i}`
|
|
256
|
+
)
|
|
257
|
+
)}
|
|
258
|
+
strategy={verticalListSortingStrategy}
|
|
259
|
+
>
|
|
260
|
+
<div
|
|
261
|
+
className={
|
|
262
|
+
(processedContent.column_blocks || []).length === 1
|
|
263
|
+
? "block"
|
|
264
|
+
: `grid gap-4
|
|
265
|
+
grid-cols-${processedContent.responsive_columns.mobile}
|
|
266
|
+
md:grid-cols-${processedContent.responsive_columns.tablet}
|
|
267
|
+
lg:grid-cols-${processedContent.responsive_columns.desktop}`
|
|
268
|
+
}
|
|
269
|
+
>
|
|
270
|
+
{Array.from({ length: (processedContent.column_blocks || []).length }, (_, columnIndex) => (
|
|
271
|
+
<ColumnEditor
|
|
272
|
+
key={`${blockType}-column-${columnIndex}`}
|
|
273
|
+
columnIndex={columnIndex}
|
|
274
|
+
blocks={getColumnBlocks(columnIndex)}
|
|
275
|
+
onBlocksChange={(newBlocks) =>
|
|
276
|
+
handleColumnBlocksChange(columnIndex, newBlocks)
|
|
277
|
+
}
|
|
278
|
+
blockType={blockType}
|
|
279
|
+
/>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</SortableContext>
|
|
283
|
+
|
|
284
|
+
{/* Drag overlay for visual feedback during cross-column dragging */}
|
|
285
|
+
<DragOverlay dropAnimation={dropAnimation}>
|
|
286
|
+
{activeId && draggedBlock ? (
|
|
287
|
+
<div className="p-2 border border-blue-300 dark:border-blue-600 rounded bg-blue-50 dark:bg-blue-900/50 shadow-lg opacity-90">
|
|
288
|
+
<div className="flex items-center gap-2">
|
|
289
|
+
<span className="text-xs font-medium text-blue-700 dark:text-blue-300 capitalize">
|
|
290
|
+
{getBlockDefinition(draggedBlock.block_type)?.label ||
|
|
291
|
+
draggedBlock.block_type}
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
|
|
295
|
+
{draggedBlock.block_type === "text" && (
|
|
296
|
+
<div
|
|
297
|
+
dangerouslySetInnerHTML={{
|
|
298
|
+
__html:
|
|
299
|
+
(
|
|
300
|
+
draggedBlock.content.html_content || "Empty text"
|
|
301
|
+
).substring(0, 30) + "...",
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
{draggedBlock.block_type === "heading" && (
|
|
306
|
+
<div>
|
|
307
|
+
H{draggedBlock.content.level || 1}:{" "}
|
|
308
|
+
{(
|
|
309
|
+
draggedBlock.content.text_content || "Empty heading"
|
|
310
|
+
).substring(0, 20) + "..."}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{draggedBlock.block_type === "image" && (
|
|
314
|
+
<div>
|
|
315
|
+
Image: {draggedBlock.content.alt_text || "No alt text"}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
{draggedBlock.block_type === "button" && (
|
|
319
|
+
<div>Button: {draggedBlock.content.text || "No text"}</div>
|
|
320
|
+
)}
|
|
321
|
+
{draggedBlock.block_type === "video_embed" && (
|
|
322
|
+
<div>Video: {draggedBlock.content.title || "No title"}</div>
|
|
323
|
+
)}
|
|
324
|
+
{draggedBlock.block_type === "posts_grid" && (
|
|
325
|
+
<div>
|
|
326
|
+
Posts Grid: {draggedBlock.content.columns || 3} cols
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
) : null}
|
|
332
|
+
</DragOverlay>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</DndContext>
|
|
336
|
+
);
|
|
337
|
+
}
|