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,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
import { Editor } from '@tiptap/react';
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import { Image as ImageIconLucide, Search, CheckCircle } from 'lucide-react';
|
|
7
|
+
import { Button } from '@nextblock-cms/ui';
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogClose,
|
|
16
|
+
} from '@nextblock-cms/ui';
|
|
17
|
+
import { Input } from '@nextblock-cms/ui';
|
|
18
|
+
import type { Database } from '@nextblock-cms/db';
|
|
19
|
+
import { createClient as createBrowserClient } from '@nextblock-cms/db';
|
|
20
|
+
|
|
21
|
+
type Media = Database['public']['Tables']['media']['Row'];
|
|
22
|
+
|
|
23
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
24
|
+
|
|
25
|
+
interface MediaLibraryModalProps {
|
|
26
|
+
editor: Editor | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
|
|
30
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
31
|
+
const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
|
|
32
|
+
const [isLoadingMedia, setIsLoadingMedia] = useState(false);
|
|
33
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
34
|
+
const supabase = createBrowserClient();
|
|
35
|
+
|
|
36
|
+
const fetchLibrary = useCallback(async () => {
|
|
37
|
+
if (!isModalOpen) return;
|
|
38
|
+
setIsLoadingMedia(true);
|
|
39
|
+
let query = supabase.from('media').select('*').order('created_at', { ascending: false }).limit(20);
|
|
40
|
+
if (searchTerm) {
|
|
41
|
+
query = query.ilike('file_name', `%${searchTerm}%`);
|
|
42
|
+
}
|
|
43
|
+
const { data, error } = await query;
|
|
44
|
+
if (data) setMediaLibrary(data);
|
|
45
|
+
else console.error("Error fetching media library:", error);
|
|
46
|
+
setIsLoadingMedia(false);
|
|
47
|
+
}, [isModalOpen, searchTerm, supabase]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
fetchLibrary();
|
|
51
|
+
}, [fetchLibrary]);
|
|
52
|
+
|
|
53
|
+
const handleSelectMedia = (mediaItem: Media) => {
|
|
54
|
+
if (editor && mediaItem.file_type?.startsWith("image/")) {
|
|
55
|
+
const imageUrl = `${R2_BASE_URL}/${mediaItem.object_key}`;
|
|
56
|
+
editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
|
|
57
|
+
}
|
|
58
|
+
setIsModalOpen(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
63
|
+
<DialogTrigger asChild>
|
|
64
|
+
<Button type="button" variant="ghost" size="icon" title="Add Image" disabled={!editor?.isEditable}>
|
|
65
|
+
<ImageIconLucide className="h-4 w-4" />
|
|
66
|
+
</Button>
|
|
67
|
+
</DialogTrigger>
|
|
68
|
+
<DialogContent className="sm:max-w-[650px] md:max-w-[800px] lg:max-w-[1000px] max-h-[80vh] flex flex-col">
|
|
69
|
+
<DialogHeader>
|
|
70
|
+
<DialogTitle>Select Image from Media Library</DialogTitle>
|
|
71
|
+
</DialogHeader>
|
|
72
|
+
<div className="relative mb-4">
|
|
73
|
+
<Input
|
|
74
|
+
type="search"
|
|
75
|
+
placeholder="Search media by name..."
|
|
76
|
+
value={searchTerm}
|
|
77
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
78
|
+
className="pl-10"
|
|
79
|
+
/>
|
|
80
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
81
|
+
</div>
|
|
82
|
+
{isLoadingMedia ? (
|
|
83
|
+
<div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
|
|
84
|
+
) : mediaLibrary.length === 0 ? (
|
|
85
|
+
<div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
|
|
86
|
+
) : (
|
|
87
|
+
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
|
|
88
|
+
{mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => (
|
|
89
|
+
<button
|
|
90
|
+
key={media.id}
|
|
91
|
+
type="button"
|
|
92
|
+
className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
|
93
|
+
onClick={() => handleSelectMedia(media)}
|
|
94
|
+
>
|
|
95
|
+
<Image
|
|
96
|
+
src={`${R2_BASE_URL}/${media.object_key}`}
|
|
97
|
+
alt={media.description || media.file_name}
|
|
98
|
+
width={200}
|
|
99
|
+
height={200}
|
|
100
|
+
className="h-full w-full object-cover"
|
|
101
|
+
/>
|
|
102
|
+
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
|
|
103
|
+
<CheckCircle className="h-8 w-8 text-white" />
|
|
104
|
+
</div>
|
|
105
|
+
<p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
|
|
106
|
+
{media.file_name}
|
|
107
|
+
</p>
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<DialogFooter className="mt-4">
|
|
113
|
+
<DialogClose asChild>
|
|
114
|
+
<Button type="button" variant="outline">Cancel</Button>
|
|
115
|
+
</DialogClose>
|
|
116
|
+
</DialogFooter>
|
|
117
|
+
</DialogContent>
|
|
118
|
+
</Dialog>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// app/cms/blocks/components/SectionConfigPanel.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Label } from "@nextblock-cms/ui";
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@nextblock-cms/ui";
|
|
7
|
+
import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
|
|
8
|
+
import BackgroundSelector from './BackgroundSelector';
|
|
9
|
+
|
|
10
|
+
interface SectionConfigPanelProps {
|
|
11
|
+
content: Partial<SectionBlockContent>;
|
|
12
|
+
onChange: (newContent: Partial<SectionBlockContent>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function SectionConfigPanel({ content, onChange }: SectionConfigPanelProps) {
|
|
16
|
+
const handleContainerTypeChange = (value: SectionBlockContent['container_type']) => {
|
|
17
|
+
onChange({
|
|
18
|
+
...content,
|
|
19
|
+
container_type: value
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleColumnGapChange = (value: SectionBlockContent['column_gap']) => {
|
|
24
|
+
onChange({
|
|
25
|
+
...content,
|
|
26
|
+
column_gap: value
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleDesktopColumnsChange = (value: string) => {
|
|
31
|
+
const desktopColumns = parseInt(value) as 1 | 2 | 3 | 4;
|
|
32
|
+
const currentBlocks = content.column_blocks || [];
|
|
33
|
+
let newColumnBlocks = [...currentBlocks];
|
|
34
|
+
|
|
35
|
+
if (desktopColumns < currentBlocks.length) {
|
|
36
|
+
newColumnBlocks = currentBlocks.slice(0, desktopColumns);
|
|
37
|
+
} else if (desktopColumns > currentBlocks.length) {
|
|
38
|
+
const columnsToAdd = desktopColumns - currentBlocks.length;
|
|
39
|
+
for (let i = 0; i < columnsToAdd; i++) {
|
|
40
|
+
newColumnBlocks.push([{
|
|
41
|
+
block_type: "text",
|
|
42
|
+
content: { html_content: `<p>New Column ${currentBlocks.length + i + 1}</p>` },
|
|
43
|
+
temp_id: `new-${Date.now()}-${i}`
|
|
44
|
+
}]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onChange({
|
|
49
|
+
...content,
|
|
50
|
+
responsive_columns: {
|
|
51
|
+
...(content.responsive_columns || { mobile: 1, tablet: 2, desktop: 3 }),
|
|
52
|
+
desktop: desktopColumns,
|
|
53
|
+
},
|
|
54
|
+
column_blocks: newColumnBlocks,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-4">
|
|
60
|
+
<div className="flex items-center justify-between">
|
|
61
|
+
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Section Configuration</h3>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<>
|
|
65
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
66
|
+
{/* Container Type */}
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<Label htmlFor="container-type">Container Type</Label>
|
|
69
|
+
<Select value={content.container_type} onValueChange={handleContainerTypeChange}>
|
|
70
|
+
<SelectTrigger id="container-type">
|
|
71
|
+
<SelectValue placeholder="Select container type" />
|
|
72
|
+
</SelectTrigger>
|
|
73
|
+
<SelectContent>
|
|
74
|
+
<SelectItem value="full-width">Full Width</SelectItem>
|
|
75
|
+
<SelectItem value="container">Container</SelectItem>
|
|
76
|
+
<SelectItem value="container-sm">Container Small</SelectItem>
|
|
77
|
+
<SelectItem value="container-lg">Container Large</SelectItem>
|
|
78
|
+
<SelectItem value="container-xl">Container XL</SelectItem>
|
|
79
|
+
</SelectContent>
|
|
80
|
+
</Select>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Desktop Columns */}
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<Label htmlFor="desktop-columns">Desktop Columns</Label>
|
|
86
|
+
<Select value={content.responsive_columns?.desktop?.toString()} onValueChange={handleDesktopColumnsChange}>
|
|
87
|
+
<SelectTrigger id="desktop-columns">
|
|
88
|
+
<SelectValue placeholder="Select columns" />
|
|
89
|
+
</SelectTrigger>
|
|
90
|
+
<SelectContent>
|
|
91
|
+
<SelectItem value="1">1 Column</SelectItem>
|
|
92
|
+
<SelectItem value="2">2 Columns</SelectItem>
|
|
93
|
+
<SelectItem value="3">3 Columns</SelectItem>
|
|
94
|
+
<SelectItem value="4">4 Columns</SelectItem>
|
|
95
|
+
</SelectContent>
|
|
96
|
+
</Select>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Column Gap */}
|
|
100
|
+
<div className="space-y-2">
|
|
101
|
+
<Label htmlFor="column-gap">Column Gap</Label>
|
|
102
|
+
<Select value={content.column_gap} onValueChange={handleColumnGapChange}>
|
|
103
|
+
<SelectTrigger id="column-gap">
|
|
104
|
+
<SelectValue placeholder="Select gap" />
|
|
105
|
+
</SelectTrigger>
|
|
106
|
+
<SelectContent>
|
|
107
|
+
<SelectItem value="none">None</SelectItem>
|
|
108
|
+
<SelectItem value="sm">Small</SelectItem>
|
|
109
|
+
<SelectItem value="md">Medium</SelectItem>
|
|
110
|
+
<SelectItem value="lg">Large</SelectItem>
|
|
111
|
+
<SelectItem value="xl">Extra Large</SelectItem>
|
|
112
|
+
</SelectContent>
|
|
113
|
+
</Select>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Background Configuration */}
|
|
118
|
+
<div className="space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
119
|
+
<h4 className="text-md font-medium text-gray-900 dark:text-gray-100">Background</h4>
|
|
120
|
+
<BackgroundSelector
|
|
121
|
+
background={content.background || { type: 'none' }}
|
|
122
|
+
onChange={(newBackground) => {
|
|
123
|
+
onChange({
|
|
124
|
+
...content,
|
|
125
|
+
background: newBackground,
|
|
126
|
+
});
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// app/cms/blocks/components/SortableBlockItem.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useSortable } from "@dnd-kit/sortable";
|
|
6
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
7
|
+
|
|
8
|
+
import EditableBlock, { EditableBlockProps } from "./EditableBlock"; // Import the actual component and its props
|
|
9
|
+
|
|
10
|
+
// interface SortableBlockItemProps extends EditableBlockProps {
|
|
11
|
+
// // No new props needed specifically for SortableBlockItem itself,
|
|
12
|
+
// // as it passes through all props to EditableBlock
|
|
13
|
+
// }
|
|
14
|
+
|
|
15
|
+
export function SortableBlockItem(props: EditableBlockProps) {
|
|
16
|
+
const {
|
|
17
|
+
attributes,
|
|
18
|
+
listeners,
|
|
19
|
+
setNodeRef,
|
|
20
|
+
transform,
|
|
21
|
+
transition,
|
|
22
|
+
isDragging,
|
|
23
|
+
} = useSortable({ id: props.block.id });
|
|
24
|
+
|
|
25
|
+
const style = {
|
|
26
|
+
transform: CSS.Transform.toString(transform),
|
|
27
|
+
transition,
|
|
28
|
+
zIndex: isDragging ? 100 : 'auto', // Ensure dragging item is on top
|
|
29
|
+
opacity: isDragging ? 0.8 : 1,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Pass the drag handle props (attributes and listeners) to the EditableBlock
|
|
33
|
+
// The EditableBlock component should then spread these onto its drag handle element.
|
|
34
|
+
// If EditableBlock doesn't have a specific handle, spread them on its root.
|
|
35
|
+
return (
|
|
36
|
+
<div ref={setNodeRef} style={style}>
|
|
37
|
+
{/*
|
|
38
|
+
Pass attributes and listeners to the element you want to be the drag handle.
|
|
39
|
+
If the whole block is draggable, pass it to the root of EditableBlock.
|
|
40
|
+
If there's a specific handle icon, pass it to that.
|
|
41
|
+
Here, we pass it as a prop, assuming EditableBlock will use it.
|
|
42
|
+
*/}
|
|
43
|
+
<EditableBlock {...props} dragHandleProps={{...attributes, ...listeners}} />
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// app/cms/blocks/editors/ButtonBlockEditor.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React from 'react'; // Added React import for JSX
|
|
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 { BlockEditorProps } from '../components/BlockEditorModal';
|
|
9
|
+
|
|
10
|
+
export type ButtonBlockContent = {
|
|
11
|
+
text?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
14
|
+
size?: 'default' | 'sm' | 'lg';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const buttonVariants: ButtonBlockContent['variant'][] = ['default', 'outline', 'secondary', 'ghost', 'link'];
|
|
18
|
+
const buttonSizes: ButtonBlockContent['size'][] = ['default', 'sm', 'lg'];
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default function ButtonBlockEditor({ content, onChange }: BlockEditorProps<Partial<ButtonBlockContent>>) {
|
|
22
|
+
|
|
23
|
+
const handleChange = (field: keyof ButtonBlockContent, value: string) => {
|
|
24
|
+
// Ensure that when variant or size is cleared, it's set to undefined or a valid default, not an empty string if your type doesn't allow it.
|
|
25
|
+
// However, the Select component's onValueChange will provide valid values from the list or an empty string if placeholder is re-selected (which shouldn't happen here).
|
|
26
|
+
onChange({ ...content, [field]: value });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-3 p-3 border-t mt-2">
|
|
31
|
+
<div>
|
|
32
|
+
<Label htmlFor="btn-text">Button Text</Label>
|
|
33
|
+
<Input
|
|
34
|
+
id="btn-text"
|
|
35
|
+
value={content.text || ""}
|
|
36
|
+
onChange={(e) => handleChange('text', e.target.value)}
|
|
37
|
+
placeholder="Learn More"
|
|
38
|
+
className="mt-1"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
<div>
|
|
42
|
+
<Label htmlFor="btn-url">Button URL</Label>
|
|
43
|
+
<Input
|
|
44
|
+
id="btn-url"
|
|
45
|
+
value={content.url || ""}
|
|
46
|
+
onChange={(e) => handleChange('url', e.target.value)}
|
|
47
|
+
placeholder="/contact-us or https://example.com"
|
|
48
|
+
className="mt-1"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<Label htmlFor="btn-variant">Variant</Label>
|
|
53
|
+
<Select
|
|
54
|
+
value={content.variant || "default"}
|
|
55
|
+
onValueChange={(val: string) => handleChange('variant', val)}
|
|
56
|
+
>
|
|
57
|
+
<SelectTrigger id="btn-variant" className="mt-1">
|
|
58
|
+
<SelectValue placeholder="Select variant" />
|
|
59
|
+
</SelectTrigger>
|
|
60
|
+
<SelectContent>
|
|
61
|
+
{buttonVariants.filter((v): v is Exclude<ButtonBlockContent['variant'], undefined> => v !== undefined).map(v => (
|
|
62
|
+
<SelectItem key={v} value={v}>{v.charAt(0).toUpperCase() + v.slice(1)}</SelectItem>
|
|
63
|
+
))}
|
|
64
|
+
</SelectContent>
|
|
65
|
+
</Select>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<Label htmlFor="btn-size">Size</Label>
|
|
69
|
+
<Select
|
|
70
|
+
value={content.size || "default"}
|
|
71
|
+
onValueChange={(val: string) => handleChange('size', val)}
|
|
72
|
+
>
|
|
73
|
+
<SelectTrigger id="btn-size" className="mt-1">
|
|
74
|
+
<SelectValue placeholder="Select size" />
|
|
75
|
+
</SelectTrigger>
|
|
76
|
+
<SelectContent>
|
|
77
|
+
{buttonSizes.filter((s): s is Exclude<ButtonBlockContent['size'], undefined> => s !== undefined).map(s => (
|
|
78
|
+
<SelectItem key={s} value={s}>{s.toUpperCase()}</SelectItem>
|
|
79
|
+
))}
|
|
80
|
+
</SelectContent>
|
|
81
|
+
</Select>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core';
|
|
5
|
+
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
6
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
7
|
+
import { GripVertical, PlusCircle, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
|
8
|
+
import { Button } from '@nextblock-cms/ui';
|
|
9
|
+
import { Input } from '@nextblock-cms/ui';
|
|
10
|
+
import { Label } from '@nextblock-cms/ui';
|
|
11
|
+
import { Checkbox } from '@nextblock-cms/ui';
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nextblock-cms/ui';
|
|
13
|
+
import { BlockEditorProps } from '@/app/cms/blocks/components/BlockEditorModal';
|
|
14
|
+
import type { FormBlockContent, FormField, FormFieldOption } from '@/lib/blocks/blockRegistry';
|
|
15
|
+
|
|
16
|
+
// Sub-component for a single editable form field in the editor
|
|
17
|
+
const SortableFormField = ({ field, index, onUpdate, onDelete }: { field: FormField, index: number, onUpdate: (index: number, field: FormField) => void, onDelete: (index: number) => void }) => {
|
|
18
|
+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: field.temp_id });
|
|
19
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
20
|
+
|
|
21
|
+
const style = {
|
|
22
|
+
transform: CSS.Transform.toString(transform),
|
|
23
|
+
transition,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleFieldChange = (prop: keyof FormField, value: unknown) => {
|
|
27
|
+
onUpdate(index, { ...field, [prop]: value });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleOptionChange = (optionIndex: number, prop: keyof FormFieldOption, value: string) => {
|
|
31
|
+
const newOptions = [...(field.options || [])];
|
|
32
|
+
newOptions[optionIndex] = { ...newOptions[optionIndex], [prop]: value };
|
|
33
|
+
handleFieldChange('options', newOptions);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const addOption = () => {
|
|
37
|
+
const newOptions = [...(field.options || []), { label: `Option ${ (field.options?.length || 0) + 1}`, value: `option-${ (field.options?.length || 0) + 1}` }];
|
|
38
|
+
handleFieldChange('options', newOptions);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const removeOption = (optionIndex: number) => {
|
|
42
|
+
const newOptions = field.options?.filter((_, i) => i !== optionIndex);
|
|
43
|
+
handleFieldChange('options', newOptions);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div ref={setNodeRef} style={style} className="p-3 border rounded bg-background shadow-sm">
|
|
48
|
+
<div className="flex items-center justify-between">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<Button variant="ghost" size="sm" {...attributes} {...listeners} className="cursor-grab p-1"><GripVertical className="h-4 w-4" /></Button>
|
|
51
|
+
<span className="font-medium text-sm">{field.label || `Field ${index + 1}`} ({field.field_type})</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex items-center gap-2">
|
|
54
|
+
<Button variant="ghost" size="icon" onClick={() => setIsExpanded(!isExpanded)} className="h-8 w-8">{isExpanded ? <ChevronUp/> : <ChevronDown/>}</Button>
|
|
55
|
+
<Button variant="destructive" size="icon" onClick={() => onDelete(index)} className="h-8 w-8"><Trash2 className="h-4 w-4" /></Button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
{isExpanded && (
|
|
59
|
+
<div className="mt-4 space-y-4 p-3 border-t">
|
|
60
|
+
<div className="grid grid-cols-2 gap-4">
|
|
61
|
+
<div>
|
|
62
|
+
<Label>Field Type</Label>
|
|
63
|
+
<Select value={field.field_type} onValueChange={(value) => handleFieldChange('field_type', value)}>
|
|
64
|
+
<SelectTrigger><SelectValue/></SelectTrigger>
|
|
65
|
+
<SelectContent>
|
|
66
|
+
<SelectItem value="text">Text</SelectItem>
|
|
67
|
+
<SelectItem value="email">Email</SelectItem>
|
|
68
|
+
<SelectItem value="textarea">Text Area</SelectItem>
|
|
69
|
+
<SelectItem value="select">Select</SelectItem>
|
|
70
|
+
<SelectItem value="radio">Radio Buttons</SelectItem>
|
|
71
|
+
<SelectItem value="checkbox">Checkbox</SelectItem>
|
|
72
|
+
</SelectContent>
|
|
73
|
+
</Select>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<Label>Label</Label>
|
|
77
|
+
<Input value={field.label} onChange={(e) => handleFieldChange('label', e.target.value)} />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<Label>Placeholder</Label>
|
|
82
|
+
<Input value={field.placeholder || ''} onChange={(e) => handleFieldChange('placeholder', e.target.value)} />
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Checkbox id={`required-${field.temp_id}`} checked={field.is_required} onCheckedChange={(checked) => handleFieldChange('is_required', checked)} />
|
|
86
|
+
<Label htmlFor={`required-${field.temp_id}`}>Required</Label>
|
|
87
|
+
</div>
|
|
88
|
+
{(field.field_type === 'select' || field.field_type === 'radio') && (
|
|
89
|
+
<div className="space-y-2">
|
|
90
|
+
<Label>Options</Label>
|
|
91
|
+
{field.options?.map((option, optIndex) => (
|
|
92
|
+
<div key={optIndex} className="flex items-center gap-2">
|
|
93
|
+
<Input value={option.label} placeholder="Label" onChange={(e) => handleOptionChange(optIndex, 'label', e.target.value)} />
|
|
94
|
+
<Input value={option.value} placeholder="Value" onChange={(e) => handleOptionChange(optIndex, 'value', e.target.value)} />
|
|
95
|
+
<Button variant="ghost" size="icon" onClick={() => removeOption(optIndex)}><Trash2 className="h-4 w-4" /></Button>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
<Button variant="outline" size="sm" onClick={addOption}><PlusCircle className="h-4 w-4 mr-2" />Add Option</Button>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default function FormBlockEditor({ content, onChange }: BlockEditorProps<Partial<FormBlockContent>>) {
|
|
108
|
+
const [fields, setFields] = useState<FormField[]>(content.fields || []);
|
|
109
|
+
|
|
110
|
+
const handleMainSettingChange = (prop: keyof FormBlockContent, value: string) => {
|
|
111
|
+
onChange({ ...content, fields, [prop]: value });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const addNewField = () => {
|
|
115
|
+
const newField: FormField = {
|
|
116
|
+
temp_id: `field-${Date.now()}`,
|
|
117
|
+
field_type: 'text',
|
|
118
|
+
label: `New Field ${fields.length + 1}`,
|
|
119
|
+
is_required: false,
|
|
120
|
+
};
|
|
121
|
+
const newFields = [...fields, newField];
|
|
122
|
+
setFields(newFields);
|
|
123
|
+
onChange({ ...content, fields: newFields });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const updateField = (index: number, updatedField: FormField) => {
|
|
127
|
+
const newFields = [...fields];
|
|
128
|
+
newFields[index] = updatedField;
|
|
129
|
+
setFields(newFields);
|
|
130
|
+
onChange({ ...content, fields: newFields });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const deleteField = (index: number) => {
|
|
134
|
+
const newFields = fields.filter((_, i) => i !== index);
|
|
135
|
+
setFields(newFields);
|
|
136
|
+
onChange({ ...content, fields: newFields });
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
140
|
+
const { active, over } = event;
|
|
141
|
+
if (over && active.id !== over.id) {
|
|
142
|
+
const oldIndex = fields.findIndex(f => f.temp_id === active.id);
|
|
143
|
+
const newIndex = fields.findIndex(f => f.temp_id === over.id);
|
|
144
|
+
const newFields = arrayMove(fields, oldIndex, newIndex);
|
|
145
|
+
setFields(newFields);
|
|
146
|
+
onChange({ ...content, fields: newFields });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="space-y-6 p-4 border-t mt-2">
|
|
152
|
+
<h3 className="text-lg font-medium">Form Settings</h3>
|
|
153
|
+
<div className="space-y-4 p-3 border rounded">
|
|
154
|
+
<div>
|
|
155
|
+
<Label>Recipient Email</Label>
|
|
156
|
+
<Input value={content.recipient_email || ''} onChange={(e) => handleMainSettingChange('recipient_email', e.target.value)} placeholder="submissions@example.com"/>
|
|
157
|
+
<p className="text-xs text-muted-foreground mt-1">The address where form submissions will be sent.</p>
|
|
158
|
+
</div>
|
|
159
|
+
<div>
|
|
160
|
+
<Label>Submit Button Text</Label>
|
|
161
|
+
<Input value={content.submit_button_text || ''} onChange={(e) => handleMainSettingChange('submit_button_text', e.target.value)} />
|
|
162
|
+
</div>
|
|
163
|
+
<div>
|
|
164
|
+
<Label>Success Message</Label>
|
|
165
|
+
<Input value={content.success_message || ''} onChange={(e) => handleMainSettingChange('success_message', e.target.value)} />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<h3 className="text-lg font-medium">Form Fields</h3>
|
|
170
|
+
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
171
|
+
<SortableContext items={fields.map(f => f.temp_id)} strategy={verticalListSortingStrategy}>
|
|
172
|
+
<div className="space-y-3">
|
|
173
|
+
{fields.map((field, index) => (
|
|
174
|
+
<SortableFormField key={field.temp_id} index={index} field={field} onUpdate={updateField} onDelete={deleteField} />
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
</SortableContext>
|
|
178
|
+
</DndContext>
|
|
179
|
+
<Button variant="outline" onClick={addNewField}><PlusCircle className="h-4 w-4 mr-2" />Add Field</Button>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|