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.
Files changed (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,348 @@
1
+ // app/cms/blocks/components/BackgroundSelector.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useEffect } from "react";
5
+ import Image from "next/image";
6
+ import { Label, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, Button, Input, Checkbox } from "@nextblock-cms/ui";
7
+ import { CustomSelectWithInput, ColorPicker } from "@nextblock-cms/ui";
8
+ import { TooltipProvider } from "@radix-ui/react-tooltip";
9
+ import { ImageIcon, X as XIcon, Save } from "lucide-react";
10
+ import { cn } from "@nextblock-cms/utils";
11
+ import type { Database } from "@nextblock-cms/db";
12
+ import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
13
+ import MediaPickerDialog from "@/app/cms/media/components/MediaPickerDialog";
14
+
15
+ type Media = Database["public"]["Tables"]["media"]["Row"];
16
+
17
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
18
+
19
+ interface BackgroundSelectorProps {
20
+ background: SectionBlockContent["background"];
21
+ onChange: (newBackground: SectionBlockContent["background"]) => void;
22
+ }
23
+
24
+ export default function BackgroundSelector({ background, onChange }: BackgroundSelectorProps) {
25
+
26
+ const backgroundType = background?.type || "none";
27
+ const selectedImage = background?.type === "image" ? background.image : undefined;
28
+ const [minHeight, setMinHeight] = useState(background?.min_height || "");
29
+ const [imagePosition, setImagePosition] = useState<string>(selectedImage?.position || "center");
30
+ const [overlayDirection, setOverlayDirection] = useState(selectedImage?.overlay?.gradient?.direction || "to bottom");
31
+
32
+ useEffect(() => {
33
+ setMinHeight(background?.min_height || "");
34
+ }, [background?.min_height]);
35
+
36
+ useEffect(() => {
37
+ setImagePosition(selectedImage?.position || "center");
38
+ setOverlayDirection(selectedImage?.overlay?.gradient?.direction || "to bottom");
39
+ }, [selectedImage?.position, selectedImage?.overlay?.gradient?.direction]);
40
+
41
+ const generateGradientCss = (gradient: { direction?: string; stops?: Array<{ color: string; position: number }> }) => {
42
+ if (!gradient || !gradient.stops || gradient.stops.length === 0) return "none";
43
+ const direction = gradient.direction || "to bottom";
44
+ const stops = gradient.stops.map((s) => `${s.color} ${s.position}%`).join(", ");
45
+ return `linear-gradient(${direction}, ${stops})`;
46
+ };
47
+
48
+ const handleTypeChange = (type: SectionBlockContent["background"]["type"]) => {
49
+ if (type === "image") {
50
+ onChange({
51
+ type: "image",
52
+ image: {
53
+ media_id: "",
54
+ object_key: "",
55
+ size: "cover",
56
+ position: "center",
57
+ overlay: undefined,
58
+ },
59
+ });
60
+ } else if (type === "gradient") {
61
+ onChange({
62
+ type: "gradient",
63
+ gradient: {
64
+ type: "linear",
65
+ direction: "to right",
66
+ stops: [
67
+ { color: "#3b82f6", position: 0 },
68
+ { color: "#8b5cf6", position: 100 },
69
+ ],
70
+ },
71
+ });
72
+ } else {
73
+ onChange({ type });
74
+ }
75
+ };
76
+
77
+ const handleSelectMediaFromLibrary = (mediaItem: Media) => {
78
+ onChange({
79
+ type: "image",
80
+ image: {
81
+ ...selectedImage,
82
+ media_id: mediaItem.id,
83
+ object_key: mediaItem.object_key,
84
+ width: mediaItem.width ?? undefined,
85
+ height: mediaItem.height ?? undefined,
86
+ size: selectedImage?.size || "cover",
87
+ position: selectedImage?.position || "center",
88
+ },
89
+ });
90
+ };
91
+
92
+ const handleRemoveImage = () => {
93
+ onChange({
94
+ type: "image",
95
+ image: {
96
+ media_id: "",
97
+ object_key: "",
98
+ size: "cover",
99
+ position: "center",
100
+ overlay: undefined,
101
+ },
102
+ });
103
+ };
104
+
105
+ const handleImagePropertyChange = (prop: "size" | "position", value: string) => {
106
+ if (background?.type === "image" && background.image) {
107
+ onChange({ ...background, image: { ...background.image, [prop]: value } });
108
+ }
109
+ };
110
+
111
+ const handleOverlayToggle = (checked: boolean) => {
112
+ if (background?.type === "image" && background.image) {
113
+ const newOverlay = checked
114
+ ? {
115
+ type: "gradient" as const,
116
+ gradient: {
117
+ type: "linear" as const,
118
+ direction: "to bottom",
119
+ stops: [
120
+ { color: "rgba(0,0,0,0.5)", position: 0 },
121
+ { color: "rgba(0,0,0,0)", position: 100 },
122
+ ],
123
+ },
124
+ }
125
+ : undefined;
126
+ onChange({ ...background, image: { ...background.image, overlay: newOverlay } });
127
+ }
128
+ };
129
+
130
+ const handleBackgroundPropertyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
131
+ const { name, value } = e.target;
132
+ onChange({ ...background, [name]: value });
133
+ };
134
+
135
+ const handleOverlayGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
136
+ const { name, value } = e.target;
137
+ if (background?.type === "image" && background.image) {
138
+ const { image } = background;
139
+ const overlay = image.overlay;
140
+ const currentGradient = overlay?.gradient || {
141
+ type: "linear" as const,
142
+ direction: "to bottom",
143
+ stops: [
144
+ { color: "rgba(0,0,0,0.5)", position: 0 },
145
+ { color: "rgba(0,0,0,0)", position: 100 },
146
+ ],
147
+ };
148
+
149
+ const updatedStops = currentGradient.stops.map((stop) => {
150
+ if (name === "startColor" && stop.position === 0) return { ...stop, color: value };
151
+ if (name === "endColor" && stop.position === 100) return { ...stop, color: value };
152
+ return stop;
153
+ });
154
+
155
+ const updatedGradient =
156
+ name === "direction"
157
+ ? { ...currentGradient, direction: value }
158
+ : { ...currentGradient, stops: updatedStops };
159
+
160
+ onChange({ ...background, image: { ...image, overlay: { type: "gradient", gradient: updatedGradient } } });
161
+ }
162
+ };
163
+
164
+ const hasMinHeightChanged = (background?.min_height || "") !== minHeight;
165
+ const imageSizeClass = selectedImage?.size === "contain" ? "object-contain" : "object-cover";
166
+ const hasOverlayDirectionChanged = (selectedImage?.overlay?.gradient?.direction || "to bottom") !== overlayDirection;
167
+
168
+ return (
169
+ <TooltipProvider>
170
+ <div className="space-y-4">
171
+ <div className="grid gap-2">
172
+ <Label>Background Type</Label>
173
+ <Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as any)}>
174
+ <SelectTrigger className="w-full max-w-[250px]"><SelectValue placeholder="Select type" /></SelectTrigger>
175
+ <SelectContent>
176
+ <SelectItem value="none">None</SelectItem>
177
+ <SelectItem value="gradient">Gradient</SelectItem>
178
+ <SelectItem value="image">Image</SelectItem>
179
+ </SelectContent>
180
+ </Select>
181
+ </div>
182
+
183
+ <div className="grid gap-2">
184
+ <Label htmlFor="min_height">Minimum Height (e.g., 250px)</Label>
185
+ <div className="flex items-center gap-2">
186
+ <Input id="min_height" name="min_height" value={minHeight} onChange={(e) => setMinHeight(e.target.value)} placeholder="e.g., 250px" className="max-w-[200px]" />
187
+ <Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } } as any)} disabled={!hasMinHeightChanged} title="Save Minimum Height">
188
+ <Save className={cn("h-5 w-5", hasMinHeightChanged && "text-green-600")} />
189
+ </Button>
190
+ </div>
191
+ </div>
192
+
193
+ {backgroundType === "image" && (
194
+ <>
195
+ <div className="mt-3 p-3 border rounded-md bg-muted/30 min-h-[120px] flex flex-col items-center justify-center">
196
+ {selectedImage?.object_key ? (
197
+ <div className="relative group w-full" style={{ height: background?.min_height || "250px", overflow: "hidden" }}>
198
+ <Image src={`${R2_BASE_URL}/${selectedImage.object_key}`} alt="Selected background image" width={selectedImage.width || 500} height={selectedImage.height || 300} sizes="100vw" className={`w-full h-full ${imageSizeClass}`} style={{ objectPosition: selectedImage.position }} />
199
+ {selectedImage.overlay && (
200
+ <div className="absolute inset-0" style={{ background: generateGradientCss(selectedImage.overlay.gradient) }} />
201
+ )}
202
+ <Button type="button" variant="destructive" size="icon" className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6" onClick={handleRemoveImage} title="Remove Image">
203
+ <XIcon className="h-3 w-3" />
204
+ </Button>
205
+ </div>
206
+ ) : (
207
+ <ImageIcon className="h-16 w-16 text-muted-foreground" />
208
+ )}
209
+
210
+ <div className="mt-3">
211
+ <MediaPickerDialog
212
+ triggerLabel={selectedImage?.object_key ? "Change Image" : "Select from Library"}
213
+ onSelect={handleSelectMediaFromLibrary}
214
+ accept={(m) => !!m.file_type?.startsWith("image/")}
215
+ title="Select or Upload Background Image"
216
+ />
217
+ </div>
218
+ </div>
219
+
220
+ <div className="grid gap-2">
221
+ <Label>Image Size</Label>
222
+ <Select value={selectedImage?.size || "cover"} onValueChange={(v) => handleImagePropertyChange("size", v)}>
223
+ <SelectTrigger className="w-full max-w-[250px]"><SelectValue /></SelectTrigger>
224
+ <SelectContent>
225
+ <SelectItem value="cover">Cover</SelectItem>
226
+ <SelectItem value="contain">Contain</SelectItem>
227
+ </SelectContent>
228
+ </Select>
229
+ </div>
230
+
231
+ <div className="grid gap-2">
232
+ <Label>Image Position</Label>
233
+ <Select value={imagePosition} onValueChange={(v) => { setImagePosition(v); handleImagePropertyChange("position", v); }}>
234
+ <SelectTrigger className="w-full max-w-[250px]"><SelectValue /></SelectTrigger>
235
+ <SelectContent>
236
+ <SelectItem value="center">Center</SelectItem>
237
+ <SelectItem value="top">Top</SelectItem>
238
+ <SelectItem value="bottom">Bottom</SelectItem>
239
+ <SelectItem value="left">Left</SelectItem>
240
+ <SelectItem value="right">Right</SelectItem>
241
+ <SelectItem value="top left">Top Left</SelectItem>
242
+ <SelectItem value="top right">Top Right</SelectItem>
243
+ <SelectItem value="bottom left">Bottom Left</SelectItem>
244
+ <SelectItem value="bottom right">Bottom Right</SelectItem>
245
+ </SelectContent>
246
+ </Select>
247
+ </div>
248
+
249
+ <div className="flex items-center space-x-2 mt-2">
250
+ <Checkbox id="gradientOverlay" checked={!!selectedImage?.overlay} onCheckedChange={(c) => handleOverlayToggle(!!c)} />
251
+ <div className="grid gap-1.5 leading-none">
252
+ <label htmlFor="gradientOverlay" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Add Gradient Overlay</label>
253
+ </div>
254
+ </div>
255
+
256
+ {selectedImage?.overlay && (
257
+ <div className="mt-3 p-3 border rounded-md bg-muted/30 space-y-4">
258
+ <div className="flex items-center gap-2">
259
+ <div className="flex-grow">
260
+ <CustomSelectWithInput
261
+ label="Direction"
262
+ tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
263
+ value={overlayDirection}
264
+ onChange={setOverlayDirection}
265
+ options={[
266
+ { value: "to bottom", label: "To Bottom" },
267
+ { value: "to top", label: "To Top" },
268
+ { value: "to left", label: "To Left" },
269
+ { value: "to right", label: "To Right" },
270
+ { value: "to bottom right", label: "To Bottom Right" },
271
+ { value: "to top left", label: "To Top Left" },
272
+ ]}
273
+ />
274
+ </div>
275
+ <Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } } as any)} disabled={!hasOverlayDirectionChanged} title="Save Overlay Direction">
276
+ <Save className={cn("h-5 w-5 mt-[1.3rem]", hasOverlayDirectionChanged && "text-green-600")} />
277
+ </Button>
278
+ </div>
279
+ <div className="flex items-center gap-4">
280
+ <ColorPicker
281
+ label="Start Color"
282
+ color={selectedImage.overlay.gradient?.stops?.[0]?.color || "rgba(0,0,0,0.5)"}
283
+ onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } } as any)}
284
+ />
285
+ <ColorPicker
286
+ label="End Color"
287
+ color={selectedImage.overlay.gradient?.stops?.[1]?.color || "rgba(0,0,0,0)"}
288
+ onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } } as any)}
289
+ />
290
+ </div>
291
+ </div>
292
+ )}
293
+ </>
294
+ )}
295
+
296
+ {backgroundType === "gradient" && (
297
+ <div className="mt-3 p-3 border rounded-md bg-muted/30 space-y-4">
298
+ <div>
299
+ <CustomSelectWithInput
300
+ label="Direction"
301
+ tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
302
+ value={background.gradient?.direction || "to right"}
303
+ onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } } as any)}
304
+ options={[
305
+ { value: "to right", label: "To Right" },
306
+ { value: "to left", label: "To Left" },
307
+ { value: "to top", label: "To Top" },
308
+ { value: "to bottom", label: "To Bottom" },
309
+ { value: "to bottom right", label: "To Bottom Right" },
310
+ { value: "to top left", label: "To Top Left" },
311
+ ]}
312
+ />
313
+ </div>
314
+ <div className="flex items-center gap-4">
315
+ <ColorPicker
316
+ label="Start Color"
317
+ color={background.gradient?.stops?.[0]?.color || "#3b82f6"}
318
+ onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } } as any)}
319
+ />
320
+ <ColorPicker
321
+ label="End Color"
322
+ color={background.gradient?.stops?.[1]?.color || "#8b5cf6"}
323
+ onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } } as any)}
324
+ />
325
+ </div>
326
+ </div>
327
+ )}
328
+ </div>
329
+ </TooltipProvider>
330
+ );
331
+ const handleBackgroundGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
332
+ const { name, value } = e.target as any;
333
+ if (backgroundType !== 'gradient') return;
334
+ const current = background.gradient || { type: 'linear' as const, direction: 'to right', stops: [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ] };
335
+ if (name === 'direction') {
336
+ onChange({ type: 'gradient', gradient: { ...current, direction: value } });
337
+ return;
338
+ }
339
+ if (name === 'startColor' || name === 'endColor') {
340
+ const updatedStops = (current.stops || [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ]).map((s) => {
341
+ if (name === 'startColor' && s.position === 0) return { ...s, color: value };
342
+ if (name === 'endColor' && s.position === 100) return { ...s, color: value };
343
+ return s;
344
+ });
345
+ onChange({ type: 'gradient', gradient: { ...current, stops: updatedStops } });
346
+ }
347
+ };
348
+ }