@jhits/plugin-blog 0.0.14 → 0.0.16

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 (208) hide show
  1. package/package.json +5 -4
  2. package/src/api/categories.ts +43 -0
  3. package/src/api/check-title.ts +60 -0
  4. package/src/api/config-handler.ts +76 -0
  5. package/src/api/handler.ts +418 -0
  6. package/src/api/index.ts +33 -0
  7. package/src/api/route.ts +116 -0
  8. package/src/api/router.ts +128 -0
  9. package/src/api-server.ts +11 -0
  10. package/src/config.ts +161 -0
  11. package/src/hooks/index.d.ts +8 -0
  12. package/src/hooks/index.d.ts.map +1 -0
  13. package/src/hooks/index.ts +9 -0
  14. package/src/hooks/useBlog.d.ts +31 -0
  15. package/src/hooks/useBlog.d.ts.map +1 -0
  16. package/src/hooks/useBlog.ts +85 -0
  17. package/src/hooks/useBlogs.d.ts +39 -0
  18. package/src/hooks/useBlogs.d.ts.map +1 -0
  19. package/src/hooks/useBlogs.ts +123 -0
  20. package/src/hooks/useCategories.d.ts +9 -0
  21. package/src/hooks/useCategories.d.ts.map +1 -0
  22. package/src/hooks/useCategories.ts +76 -0
  23. package/src/index.server.ts +14 -0
  24. package/src/index.tsx +335 -0
  25. package/src/init.tsx +63 -0
  26. package/src/lib/blocks/BlockRenderer.d.ts +54 -0
  27. package/src/lib/blocks/BlockRenderer.d.ts.map +1 -0
  28. package/src/lib/blocks/BlockRenderer.tsx +141 -0
  29. package/src/lib/blocks/index.ts +6 -0
  30. package/src/lib/config-storage.d.ts +30 -0
  31. package/src/lib/config-storage.d.ts.map +1 -0
  32. package/src/lib/config-storage.ts +65 -0
  33. package/src/lib/index.ts +9 -0
  34. package/src/lib/layouts/blocks/ColumnsBlock.d.ts +25 -0
  35. package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -0
  36. package/src/lib/layouts/blocks/ColumnsBlock.tsx +298 -0
  37. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
  38. package/src/lib/layouts/blocks/SectionBlock.d.ts +25 -0
  39. package/src/lib/layouts/blocks/SectionBlock.d.ts.map +1 -0
  40. package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
  41. package/src/lib/layouts/blocks/index.ts +8 -0
  42. package/src/lib/layouts/index.d.ts +23 -0
  43. package/src/lib/layouts/index.d.ts.map +1 -0
  44. package/src/lib/layouts/index.ts +52 -0
  45. package/src/lib/layouts/registerLayoutBlocks.d.ts +9 -0
  46. package/src/lib/layouts/registerLayoutBlocks.d.ts.map +1 -0
  47. package/src/lib/layouts/registerLayoutBlocks.ts +64 -0
  48. package/src/lib/mappers/apiMapper.d.ts +66 -0
  49. package/src/lib/mappers/apiMapper.d.ts.map +1 -0
  50. package/src/lib/mappers/apiMapper.ts +254 -0
  51. package/src/lib/migration/index.ts +6 -0
  52. package/src/lib/migration/mapper.ts +140 -0
  53. package/src/lib/rich-text/RichTextEditor.d.ts +45 -0
  54. package/src/lib/rich-text/RichTextEditor.d.ts.map +1 -0
  55. package/src/lib/rich-text/RichTextEditor.tsx +826 -0
  56. package/src/lib/rich-text/RichTextPreview.d.ts +16 -0
  57. package/src/lib/rich-text/RichTextPreview.d.ts.map +1 -0
  58. package/src/lib/rich-text/RichTextPreview.tsx +210 -0
  59. package/src/lib/rich-text/index.d.ts +9 -0
  60. package/src/lib/rich-text/index.d.ts.map +1 -0
  61. package/src/lib/rich-text/index.ts +10 -0
  62. package/src/lib/utils/blockHelpers.d.ts +23 -0
  63. package/src/lib/utils/blockHelpers.d.ts.map +1 -0
  64. package/src/lib/utils/blockHelpers.ts +72 -0
  65. package/src/lib/utils/configValidation.d.ts +23 -0
  66. package/src/lib/utils/configValidation.d.ts.map +1 -0
  67. package/src/lib/utils/configValidation.ts +137 -0
  68. package/src/lib/utils/index.ts +8 -0
  69. package/src/lib/utils/slugify.ts +79 -0
  70. package/src/registry/BlockRegistry.d.ts +62 -0
  71. package/src/registry/BlockRegistry.d.ts.map +1 -0
  72. package/src/registry/BlockRegistry.ts +139 -0
  73. package/src/registry/index.d.ts +6 -0
  74. package/src/registry/index.d.ts.map +1 -0
  75. package/src/registry/index.ts +11 -0
  76. package/src/state/EditorContext.d.ts +45 -0
  77. package/src/state/EditorContext.d.ts.map +1 -0
  78. package/src/state/EditorContext.tsx +283 -0
  79. package/src/state/index.d.ts +7 -0
  80. package/src/state/index.d.ts.map +1 -0
  81. package/src/state/index.ts +8 -0
  82. package/src/state/reducer.d.ts +11 -0
  83. package/src/state/reducer.d.ts.map +1 -0
  84. package/src/state/reducer.ts +694 -0
  85. package/src/state/types.d.ts +162 -0
  86. package/src/state/types.d.ts.map +1 -0
  87. package/src/state/types.ts +160 -0
  88. package/src/types/block.d.ts +221 -0
  89. package/src/types/block.d.ts.map +1 -0
  90. package/src/types/block.ts +269 -0
  91. package/src/types/index.d.ts +8 -0
  92. package/src/types/index.d.ts.map +1 -0
  93. package/src/types/index.ts +17 -0
  94. package/src/types/post.d.ts +136 -0
  95. package/src/types/post.d.ts.map +1 -0
  96. package/src/types/post.ts +169 -0
  97. package/src/utils/client.d.ts +48 -0
  98. package/src/utils/client.d.ts.map +1 -0
  99. package/src/utils/client.ts +122 -0
  100. package/src/utils/index.ts +7 -0
  101. package/src/views/CanvasEditor/BlockWrapper.d.ts +16 -0
  102. package/src/views/CanvasEditor/BlockWrapper.d.ts.map +1 -0
  103. package/src/views/CanvasEditor/BlockWrapper.tsx +522 -0
  104. package/src/views/CanvasEditor/CanvasEditorView.d.ts +14 -0
  105. package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -0
  106. package/src/views/CanvasEditor/CanvasEditorView.tsx +337 -0
  107. package/src/views/CanvasEditor/EditorBody.d.ts +22 -0
  108. package/src/views/CanvasEditor/EditorBody.d.ts.map +1 -0
  109. package/src/views/CanvasEditor/EditorBody.tsx +665 -0
  110. package/src/views/CanvasEditor/EditorHeader.d.ts +18 -0
  111. package/src/views/CanvasEditor/EditorHeader.d.ts.map +1 -0
  112. package/src/views/CanvasEditor/EditorHeader.tsx +268 -0
  113. package/src/views/CanvasEditor/LayoutContainer.d.ts +17 -0
  114. package/src/views/CanvasEditor/LayoutContainer.d.ts.map +1 -0
  115. package/src/views/CanvasEditor/LayoutContainer.tsx +322 -0
  116. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +13 -0
  117. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +1 -0
  118. package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
  119. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +14 -0
  120. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +1 -0
  121. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
  122. package/src/views/CanvasEditor/components/EditorCanvas.d.ts +29 -0
  123. package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -0
  124. package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
  125. package/src/views/CanvasEditor/components/EditorLibrary.d.ts +7 -0
  126. package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -0
  127. package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
  128. package/src/views/CanvasEditor/components/EditorSidebar.d.ts +13 -0
  129. package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -0
  130. package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
  131. package/src/views/CanvasEditor/components/ErrorBanner.d.ts +6 -0
  132. package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +1 -0
  133. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  134. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +25 -0
  135. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -0
  136. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +341 -0
  137. package/src/views/CanvasEditor/components/LibraryItem.d.ts +14 -0
  138. package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +1 -0
  139. package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
  140. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +15 -0
  141. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -0
  142. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
  143. package/src/views/CanvasEditor/components/index.d.ts +21 -0
  144. package/src/views/CanvasEditor/components/index.d.ts.map +1 -0
  145. package/src/views/CanvasEditor/components/index.ts +28 -0
  146. package/src/views/CanvasEditor/hooks/index.d.ts +10 -0
  147. package/src/views/CanvasEditor/hooks/index.d.ts.map +1 -0
  148. package/src/views/CanvasEditor/hooks/index.ts +10 -0
  149. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +8 -0
  150. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -0
  151. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
  152. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +3 -0
  153. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +1 -0
  154. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
  155. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +5 -0
  156. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -0
  157. package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
  158. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +2 -0
  159. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -0
  160. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
  161. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +25 -0
  162. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +1 -0
  163. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
  164. package/src/views/CanvasEditor/index.d.ts +16 -0
  165. package/src/views/CanvasEditor/index.d.ts.map +1 -0
  166. package/src/views/CanvasEditor/index.ts +16 -0
  167. package/src/views/PostManager/EmptyState.d.ts +10 -0
  168. package/src/views/PostManager/EmptyState.d.ts.map +1 -0
  169. package/src/views/PostManager/EmptyState.tsx +42 -0
  170. package/src/views/PostManager/PostActionsMenu.d.ts +12 -0
  171. package/src/views/PostManager/PostActionsMenu.d.ts.map +1 -0
  172. package/src/views/PostManager/PostActionsMenu.tsx +112 -0
  173. package/src/views/PostManager/PostCards.d.ts +15 -0
  174. package/src/views/PostManager/PostCards.d.ts.map +1 -0
  175. package/src/views/PostManager/PostCards.tsx +197 -0
  176. package/src/views/PostManager/PostFilters.d.ts +16 -0
  177. package/src/views/PostManager/PostFilters.d.ts.map +1 -0
  178. package/src/views/PostManager/PostFilters.tsx +95 -0
  179. package/src/views/PostManager/PostManagerView.d.ts +11 -0
  180. package/src/views/PostManager/PostManagerView.d.ts.map +1 -0
  181. package/src/views/PostManager/PostManagerView.tsx +289 -0
  182. package/src/views/PostManager/PostStats.d.ts +11 -0
  183. package/src/views/PostManager/PostStats.d.ts.map +1 -0
  184. package/src/views/PostManager/PostStats.tsx +81 -0
  185. package/src/views/PostManager/PostTable.d.ts +15 -0
  186. package/src/views/PostManager/PostTable.d.ts.map +1 -0
  187. package/src/views/PostManager/PostTable.tsx +230 -0
  188. package/src/views/PostManager/index.d.ts +12 -0
  189. package/src/views/PostManager/index.d.ts.map +1 -0
  190. package/src/views/PostManager/index.ts +15 -0
  191. package/src/views/Preview/PreviewBridgeView.d.ts +12 -0
  192. package/src/views/Preview/PreviewBridgeView.d.ts.map +1 -0
  193. package/src/views/Preview/PreviewBridgeView.tsx +64 -0
  194. package/src/views/Preview/index.d.ts +6 -0
  195. package/src/views/Preview/index.d.ts.map +1 -0
  196. package/src/views/Preview/index.ts +7 -0
  197. package/src/views/Settings/SettingsView.d.ts +10 -0
  198. package/src/views/Settings/SettingsView.d.ts.map +1 -0
  199. package/src/views/Settings/SettingsView.tsx +298 -0
  200. package/src/views/Settings/index.d.ts +6 -0
  201. package/src/views/Settings/index.d.ts.map +1 -0
  202. package/src/views/Settings/index.ts +7 -0
  203. package/src/views/SlugSEO/SlugSEOManagerView.d.ts +12 -0
  204. package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -0
  205. package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
  206. package/src/views/SlugSEO/index.d.ts +6 -0
  207. package/src/views/SlugSEO/index.d.ts.map +1 -0
  208. package/src/views/SlugSEO/index.ts +7 -0
@@ -0,0 +1,268 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { ArrowLeft, Library, Settings2, Save, Clock, Edit, Eye } from 'lucide-react';
5
+ import { useEditor } from '../../state/EditorContext';
6
+ import { SaveConfirmationModal } from './SaveConfirmationModal';
7
+
8
+ export interface EditorHeaderProps {
9
+ isLibraryOpen: boolean;
10
+ onLibraryToggle: () => void;
11
+ isPreviewMode: boolean;
12
+ onPreviewToggle: () => void;
13
+ isSidebarOpen: boolean;
14
+ onSidebarToggle: () => void;
15
+ isSaving: boolean;
16
+ onSave: (publish?: boolean) => Promise<void>;
17
+ onSaveError: (error: string | null) => void;
18
+ autoSaveEnabled?: boolean;
19
+ onAutoSaveToggle?: (enabled: boolean) => void;
20
+ isDirty?: boolean;
21
+ autoSaveCountdown?: number | null;
22
+ autoSaveStatus?: 'idle' | 'saving' | 'saved' | 'error';
23
+ }
24
+
25
+ export function EditorHeader({
26
+ isLibraryOpen,
27
+ onLibraryToggle,
28
+ isPreviewMode,
29
+ onPreviewToggle,
30
+ isSidebarOpen,
31
+ onSidebarToggle,
32
+ isSaving,
33
+ onSave,
34
+ onSaveError,
35
+ autoSaveEnabled = false,
36
+ onAutoSaveToggle,
37
+ isDirty = false,
38
+ autoSaveCountdown = null,
39
+ autoSaveStatus = 'idle',
40
+ }: EditorHeaderProps) {
41
+ const { state, dispatch } = useEditor();
42
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
43
+ const [saveAsDraft, setSaveAsDraft] = useState(false);
44
+ const [saveError, setSaveError] = useState<string | null>(null);
45
+
46
+ const handleSaveDraftClick = () => {
47
+ setSaveAsDraft(true);
48
+ setSaveError(null); // Clear any previous errors
49
+ setShowConfirmModal(true);
50
+ };
51
+
52
+ const handlePublishClick = () => {
53
+ setSaveAsDraft(false);
54
+ setSaveError(null); // Clear any previous errors
55
+ setShowConfirmModal(true);
56
+ };
57
+
58
+ const handleConfirmSave = async () => {
59
+ try {
60
+ const targetStatus = saveAsDraft ? 'draft' : 'published';
61
+ console.log('[EditorHeader] Starting save process...', { saveAsDraft, targetStatus, currentStatus: state.status });
62
+
63
+ // Set status before saving - ensure state is updated
64
+ if (saveAsDraft) {
65
+ dispatch({ type: 'SET_STATUS', payload: 'draft' });
66
+ } else {
67
+ dispatch({ type: 'SET_STATUS', payload: 'published' });
68
+ }
69
+
70
+ // Wait longer to ensure state update propagates through the reducer and context
71
+ // React state updates are asynchronous, so we need to wait for the state to actually update
72
+ await new Promise(resolve => setTimeout(resolve, 150));
73
+
74
+ // Verify status was updated
75
+ console.log('[EditorHeader] Status after update:', state.status, 'Expected:', targetStatus);
76
+
77
+ await onSave(!saveAsDraft);
78
+ console.log('[EditorHeader] Post saved successfully');
79
+ // Clear any previous errors
80
+ setSaveError(null);
81
+ // Modal will show success message and close automatically
82
+ } catch (error: any) {
83
+ console.error('[EditorHeader] Failed to save post:', error);
84
+ // Extract user-friendly error message
85
+ let errorMessage = error.message || 'Failed to save post';
86
+
87
+ // Make error messages more user-friendly
88
+ if (errorMessage.includes('Missing required fields')) {
89
+ // Keep the detailed message about missing fields
90
+ errorMessage = errorMessage.replace('Missing required fields for publishing:', 'To publish, please fill in:');
91
+ } else if (errorMessage.includes('All required fields')) {
92
+ errorMessage = 'To publish, please fill in all required fields: summary, featured image, category, and content.';
93
+ } else if (errorMessage.includes('Unauthorized')) {
94
+ errorMessage = 'You are not authorized to save this post. Please log in again.';
95
+ } else if (errorMessage.includes('Failed to save')) {
96
+ errorMessage = 'Unable to save the post. Please check your connection and try again.';
97
+ }
98
+
99
+ setSaveError(errorMessage);
100
+ onSaveError(errorMessage);
101
+ // Re-throw the error so the modal knows it failed and doesn't show success
102
+ throw error;
103
+ }
104
+ };
105
+
106
+ return (
107
+ <header className="flex items-center justify-between px-6 py-3 bg-dashboard-sidebar backdrop-blur-md border-b border-dashboard-border flex-none shrink-0">
108
+ <div className="flex items-center gap-6">
109
+ <button
110
+ onClick={() => {
111
+ if (isDirty) {
112
+ const confirmed = window.confirm(
113
+ 'You have unsaved changes. Are you sure you want to leave? Your changes will be lost.'
114
+ );
115
+ if (!confirmed) {
116
+ return;
117
+ }
118
+ }
119
+ window.location.href = '/dashboard/blog';
120
+ }}
121
+ className="text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white transition-colors"
122
+ >
123
+ <ArrowLeft size={20} strokeWidth={1.5} />
124
+ </button>
125
+ <div className="h-4 w-[1px] bg-neutral-300 dark:border-neutral-700" />
126
+ <button
127
+ onClick={onLibraryToggle}
128
+ className={`flex items-center gap-2 text-[10px] uppercase tracking-widest font-black transition-all ${isLibraryOpen ? 'text-dashboard-text' : 'text-neutral-500 dark:text-neutral-400'
129
+ }`}
130
+ >
131
+ <Library size={16} strokeWidth={1.5} />
132
+ Library
133
+ </button>
134
+ </div>
135
+
136
+ <div className="flex items-center gap-4">
137
+ {/* Auto-save Toggle */}
138
+ {onAutoSaveToggle && (
139
+ <div className="flex items-center gap-2">
140
+ <button
141
+ onClick={() => onAutoSaveToggle(!autoSaveEnabled)}
142
+ className={`relative flex items-center gap-2 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
143
+ autoSaveEnabled
144
+ ? 'bg-primary/20 text-primary border border-primary/30'
145
+ : 'bg-dashboard-bg text-neutral-600 dark:text-neutral-400 border border-dashboard-border hover:text-neutral-950 dark:hover:text-white'
146
+ }`}
147
+ title={autoSaveEnabled ? 'Auto-save enabled (saves after 10s of inactivity)' : 'Click to enable auto-save'}
148
+ >
149
+ <Clock size={12} className={autoSaveEnabled && autoSaveStatus !== 'saving' ? 'animate-pulse' : ''} />
150
+ <span>Auto-save</span>
151
+ <span className={`ml-1 text-[9px] ${autoSaveEnabled ? 'text-primary' : 'text-neutral-500 dark:text-neutral-400'}`}>
152
+ {autoSaveEnabled ? 'ON' : 'OFF'}
153
+ </span>
154
+ {/* Countdown or Status */}
155
+ {autoSaveEnabled && isDirty && (
156
+ <span className="ml-1.5 text-[9px] font-bold tabular-nums">
157
+ {autoSaveStatus === 'saving' && (
158
+ <span className="text-primary animate-pulse">Saving...</span>
159
+ )}
160
+ {autoSaveStatus === 'saved' && (
161
+ <span className="text-green-500 dark:text-green-400">Saved!</span>
162
+ )}
163
+ {autoSaveStatus === 'error' && (
164
+ <span className="text-red-500 dark:text-red-400">Error</span>
165
+ )}
166
+ {autoSaveStatus === 'idle' && autoSaveCountdown !== null && (
167
+ <span className="text-primary/70">{autoSaveCountdown}s</span>
168
+ )}
169
+ </span>
170
+ )}
171
+ </button>
172
+ {/* Unsaved Changes Indicator - only show when auto-save is off */}
173
+ {isDirty && !autoSaveEnabled && (
174
+ <span className="text-[10px] text-amber-500 dark:text-amber-400 font-bold uppercase tracking-widest animate-pulse">
175
+ Unsaved
176
+ </span>
177
+ )}
178
+ </div>
179
+ )}
180
+ {/* Edit/Preview Toggle - Segmented Control Style */}
181
+ <div className="flex items-center bg-dashboard-bg border border-dashboard-border rounded-full p-1 gap-1">
182
+ <button
183
+ onClick={() => {
184
+ if (isPreviewMode) {
185
+ onPreviewToggle();
186
+ }
187
+ }}
188
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
189
+ !isPreviewMode
190
+ ? 'bg-primary text-white shadow-sm'
191
+ : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
192
+ }`}
193
+ title="Edit mode - Make changes to your post"
194
+ >
195
+ <Edit size={12} strokeWidth={2.5} />
196
+ <span>Edit</span>
197
+ </button>
198
+ <button
199
+ onClick={() => {
200
+ if (!isPreviewMode) {
201
+ onPreviewToggle();
202
+ }
203
+ }}
204
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
205
+ isPreviewMode
206
+ ? 'bg-primary text-white shadow-sm'
207
+ : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
208
+ }`}
209
+ title="Preview mode - See how your post will look"
210
+ >
211
+ <Eye size={12} strokeWidth={2.5} />
212
+ <span>Preview</span>
213
+ </button>
214
+ </div>
215
+ {/* Save Draft Button - Always visible for drafts and new posts */}
216
+ {(state.status === 'draft' || !state.postId) && (
217
+ <button
218
+ onClick={handleSaveDraftClick}
219
+ disabled={isSaving}
220
+ className={`px-4 py-2 border-2 border-dashboard-border text-dashboard-text rounded-full text-[10px] font-bold uppercase tracking-widest transition-all ${isSaving
221
+ ? 'opacity-50 cursor-not-allowed'
222
+ : 'hover:bg-dashboard-bg'
223
+ }`}
224
+ >
225
+ {isSaving ? 'Saving...' : 'Save Draft'}
226
+ </button>
227
+ )}
228
+ {/* Publish/Update Button */}
229
+ <button
230
+ onClick={handlePublishClick}
231
+ disabled={isSaving}
232
+ className={`px-6 py-2 bg-primary text-white rounded-full text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-primary/20 ${isSaving
233
+ ? 'opacity-50 cursor-not-allowed'
234
+ : 'hover:bg-primary/90'
235
+ }`}
236
+ >
237
+ {isSaving ? 'Saving...' : state.status === 'published' ? 'Update Post' : 'Publish Post'}
238
+ </button>
239
+ <button
240
+ onClick={onSidebarToggle}
241
+ className={`p-2 rounded-full transition-colors ${isSidebarOpen
242
+ ? 'bg-dashboard-bg text-dashboard-text'
243
+ : 'text-neutral-500 dark:text-neutral-400 hover:bg-dashboard-bg'
244
+ }`}
245
+ >
246
+ <Settings2 size={18} />
247
+ </button>
248
+ </div>
249
+
250
+ {/* Save Confirmation Modal */}
251
+ <SaveConfirmationModal
252
+ isOpen={showConfirmModal}
253
+ onClose={() => {
254
+ setShowConfirmModal(false);
255
+ setSaveAsDraft(false);
256
+ setSaveError(null);
257
+ }}
258
+ onConfirm={handleConfirmSave}
259
+ isSaving={isSaving}
260
+ postTitle={state.title || undefined}
261
+ isPublished={state.status === 'published'}
262
+ saveAsDraft={saveAsDraft}
263
+ error={saveError}
264
+ />
265
+ </header>
266
+ );
267
+ }
268
+
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Layout Container Component
3
+ * Recursive drop zone for nested blocks
4
+ */
5
+ import { Block } from '../../types/block';
6
+ export interface LayoutContainerProps {
7
+ blocks: Block[];
8
+ containerId: string;
9
+ onBlockAdd: (type: string, index: number, containerId: string) => void;
10
+ onBlockUpdate: (id: string, data: Partial<Block['data']>, containerId: string) => void;
11
+ onBlockDelete: (id: string, containerId: string) => void;
12
+ onBlockMove: (id: string, newIndex: number, containerId: string) => void;
13
+ className?: string;
14
+ emptyLabel?: string;
15
+ }
16
+ export declare function LayoutContainer({ blocks, containerId, onBlockAdd, onBlockUpdate, onBlockDelete, onBlockMove, className, emptyLabel, }: LayoutContainerProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=LayoutContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LayoutContainer.d.ts","sourceRoot":"","sources":["LayoutContainer.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAI1C,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACvE,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACvF,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,eAAe,CAAC,EAC5B,MAAM,EACN,WAAW,EACX,UAAU,EACV,aAAa,EACb,aAAa,EACb,WAAW,EACX,SAAc,EACd,UAA+B,GAClC,EAAE,oBAAoB,2CA6OtB"}
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Layout Container Component
3
+ * Recursive drop zone for nested blocks
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useState, useEffect, useRef } from 'react';
9
+ import { Plus } from 'lucide-react';
10
+ import { Block } from '../../types/block';
11
+ import { BlockWrapper } from './BlockWrapper';
12
+ import { useEditor } from '../../state/EditorContext';
13
+
14
+ export interface LayoutContainerProps {
15
+ blocks: Block[];
16
+ containerId: string;
17
+ onBlockAdd: (type: string, index: number, containerId: string) => void;
18
+ onBlockUpdate: (id: string, data: Partial<Block['data']>, containerId: string) => void;
19
+ onBlockDelete: (id: string, containerId: string) => void;
20
+ onBlockMove: (id: string, newIndex: number, containerId: string) => void;
21
+ className?: string;
22
+ emptyLabel?: string;
23
+ }
24
+
25
+ export function LayoutContainer({
26
+ blocks,
27
+ containerId,
28
+ onBlockAdd,
29
+ onBlockUpdate,
30
+ onBlockDelete,
31
+ onBlockMove,
32
+ className = '',
33
+ emptyLabel = 'Drop blocks here',
34
+ }: LayoutContainerProps) {
35
+ const { darkMode } = useEditor();
36
+
37
+ // --- State ---
38
+ const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
39
+ const [dropAtEnd, setDropAtEnd] = useState(false);
40
+ const [isDragging, setIsDragging] = useState(false);
41
+ const [dropIndicatorPosition, setDropIndicatorPosition] = useState<{ top: number; left: number; width: number } | null>(null);
42
+
43
+ const containerRef = useRef<HTMLDivElement>(null);
44
+ const blockRefs = useRef<Map<string, HTMLDivElement>>(new Map());
45
+ // Use ref to ensure we always have the latest dropAtEnd value (React state updates are async)
46
+ const dropAtEndRef = useRef(false);
47
+
48
+ // --- Cleanup & Event Listeners ---
49
+ useEffect(() => {
50
+ const resetState = () => {
51
+ setDropIndicatorPosition(null);
52
+ setDragOverIndex(null);
53
+ setDropAtEnd(false);
54
+ dropAtEndRef.current = false; // Reset ref too
55
+ setIsDragging(false);
56
+ // Clear global dragged block ID on dragend (in case drag was cancelled)
57
+ if (typeof window !== 'undefined') {
58
+ (window as any).__DRAGGED_BLOCK_ID__ = null;
59
+ }
60
+ };
61
+
62
+ const container = containerRef.current;
63
+ if (container) {
64
+ container.addEventListener('clear-drop-indicator', resetState);
65
+ document.addEventListener('dragend', resetState);
66
+ return () => {
67
+ container.removeEventListener('clear-drop-indicator', resetState);
68
+ document.removeEventListener('dragend', resetState);
69
+ };
70
+ }
71
+ }, []);
72
+
73
+ // --- Drag & Drop Logic ---
74
+
75
+ const handleDragOverBlock = (e: React.DragEvent, index: number, element: HTMLElement) => {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+
79
+ // 1. Check for deeper nested containers
80
+ const target = e.target as HTMLElement;
81
+ const deeperContainer = target.closest('[data-layout-container]');
82
+ if (deeperContainer && deeperContainer !== containerRef.current) {
83
+ setDropIndicatorPosition(null);
84
+ return;
85
+ }
86
+
87
+ // 2. Notify parent containers to hide their indicators
88
+ e.currentTarget.dispatchEvent(new CustomEvent('clear-drop-indicator', { bubbles: true }));
89
+
90
+ // 3. Calculate "Above" vs "Below" and position indicator between blocks
91
+ const containerRect = containerRef.current!.getBoundingClientRect();
92
+ const elementRect = element.getBoundingClientRect();
93
+ const mouseRelativeToBlock = e.clientY - elementRect.top;
94
+ const isBottomHalf = mouseRelativeToBlock > (elementRect.height / 2);
95
+
96
+ setDragOverIndex(index);
97
+ setDropAtEnd(isBottomHalf);
98
+ dropAtEndRef.current = isBottomHalf; // Update ref immediately
99
+ setIsDragging(true);
100
+
101
+ // 4. Update Visual Indicator - always show between blocks
102
+ const elementTop = elementRect.top - containerRect.top;
103
+ const elementBottom = elementRect.bottom - containerRect.top;
104
+ let indicatorTop: number;
105
+
106
+ if (isBottomHalf) {
107
+ // Show below this block - position between current block and next block
108
+ if (index === blocks.length - 1) {
109
+ // Last block - show after it
110
+ indicatorTop = elementBottom;
111
+ } else {
112
+ // Get next block to find the gap
113
+ const nextBlock = blocks[index + 1];
114
+ const nextBlockEl = blockRefs.current.get(nextBlock.id);
115
+ if (nextBlockEl) {
116
+ const nextBlockRect = nextBlockEl.getBoundingClientRect();
117
+ const nextBlockTop = nextBlockRect.top - containerRect.top;
118
+ // Position in the middle of the gap (mb-4 = 16px margin)
119
+ indicatorTop = elementBottom + (nextBlockTop - elementBottom) / 2;
120
+ } else {
121
+ indicatorTop = elementBottom;
122
+ }
123
+ }
124
+ } else {
125
+ // Show above this block - position between previous block and current block
126
+ if (index === 0) {
127
+ // First block - show at top of container (before first block)
128
+ indicatorTop = 0;
129
+ } else {
130
+ // Get previous block to find the gap
131
+ const prevBlock = blocks[index - 1];
132
+ const prevBlockEl = blockRefs.current.get(prevBlock.id);
133
+ if (prevBlockEl) {
134
+ const prevBlockRect = prevBlockEl.getBoundingClientRect();
135
+ const prevBlockBottom = prevBlockRect.bottom - containerRect.top;
136
+ // Position in the middle of the gap (mb-4 = 16px margin)
137
+ indicatorTop = prevBlockBottom + (elementTop - prevBlockBottom) / 2;
138
+ } else {
139
+ indicatorTop = elementTop;
140
+ }
141
+ }
142
+ }
143
+
144
+ setDropIndicatorPosition({
145
+ top: indicatorTop,
146
+ left: 0,
147
+ width: containerRect.width,
148
+ });
149
+ };
150
+
151
+ const handleDrop = (e: React.DragEvent, index: number | null) => {
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+
155
+ const blockId = e.dataTransfer.getData('block-id') || (window as any).__DRAGGED_BLOCK_ID__;
156
+ const blockType = e.dataTransfer.getData('block-type');
157
+
158
+ // Clear the global dragged block ID immediately to prevent it from being used for new blocks
159
+ if (typeof window !== 'undefined') {
160
+ (window as any).__DRAGGED_BLOCK_ID__ = null;
161
+ }
162
+
163
+ // Logic: index is null when dropping on the container background (appends to end)
164
+ // When dropAtEnd is true, we want to place it AFTER the block at index, so targetIndex = index + 1
165
+ // When dropAtEnd is false, we want to place it BEFORE the block at index, so targetIndex = index
166
+ // Use ref to get the latest value (React state updates are async)
167
+ const isDropAtEnd = dropAtEndRef.current;
168
+ let targetIndex = index === null ? blocks.length : (isDropAtEnd ? index + 1 : index);
169
+
170
+ if (blockId) {
171
+ const currentIndex = blocks.findIndex(b => b.id === blockId);
172
+ if (currentIndex !== -1) {
173
+ // Moving within the same array - need to adjust for removal
174
+ let finalMoveIndex = targetIndex;
175
+
176
+ if (currentIndex < targetIndex) {
177
+ // Moving forward: when we remove the item from currentIndex, everything after it shifts down by 1.
178
+ if (isDropAtEnd) {
179
+ // Dropping below: we want it at index + 1 in the original array
180
+ // If currentIndex <= index: after removal, block at index stays at index, so we want index + 1 = targetIndex
181
+ // If index < currentIndex < targetIndex: after removal, we still want index + 1, but since we removed
182
+ // an item before targetIndex, the position targetIndex in original = targetIndex - 1 in new array
183
+ if (index !== null && currentIndex <= index) {
184
+ // Item is at or before target block - no adjustment needed
185
+ finalMoveIndex = targetIndex;
186
+ } else {
187
+ // Item is after target block but before targetIndex - need to adjust
188
+ finalMoveIndex = targetIndex - 1;
189
+ }
190
+ } else {
191
+ // Dropping above: targetIndex = index means "before the block at index"
192
+ // After removal, if currentIndex < index, the block at index shifts to index - 1,
193
+ // so we want it at index - 1 in the new array.
194
+ finalMoveIndex = targetIndex - 1;
195
+ }
196
+ }
197
+ // If currentIndex >= targetIndex, no adjustment needed (moving backward or same position)
198
+
199
+ console.log('[LayoutContainer] Drop calculation:', {
200
+ blockId,
201
+ index,
202
+ dropAtEnd: isDropAtEnd,
203
+ currentIndex,
204
+ targetIndex,
205
+ finalMoveIndex,
206
+ blocksCount: blocks.length
207
+ });
208
+
209
+ onBlockMove(blockId, Math.max(0, finalMoveIndex), containerId);
210
+ } else {
211
+ // Moving from another container - no adjustment needed
212
+ onBlockMove(blockId, targetIndex, containerId);
213
+ }
214
+ } else if (blockType) {
215
+ // Adding new block - use targetIndex as-is
216
+ onBlockAdd(blockType, targetIndex, containerId);
217
+ }
218
+
219
+ // Clean up
220
+ setDropIndicatorPosition(null);
221
+ setDragOverIndex(null);
222
+ setDropAtEnd(false);
223
+ };
224
+
225
+ const setBlockRef = (id: string) => (el: HTMLDivElement | null) => {
226
+ if (el) blockRefs.current.set(id, el); else blockRefs.current.delete(id);
227
+ };
228
+
229
+ return (
230
+ <div
231
+ ref={containerRef}
232
+ data-layout-container={containerId}
233
+ className={`relative flex flex-col min-h-[40px] transition-colors ${className}`}
234
+ onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
235
+ onDrop={(e) => handleDrop(e, null)}
236
+ onDragLeave={(e) => {
237
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
238
+ setDropIndicatorPosition(null);
239
+ }
240
+ }}
241
+ >
242
+ {/* 1. Visual Indicator Overlay */}
243
+ {dropIndicatorPosition && isDragging && (
244
+ <DropIndicator position={dropIndicatorPosition} darkMode={darkMode} />
245
+ )}
246
+
247
+ {/* 2. Content Area */}
248
+ {blocks.length === 0 ? (
249
+ <EmptyState isDragging={isDragging} darkMode={darkMode} label={emptyLabel} />
250
+ ) : (
251
+ blocks.map((block, index) => (
252
+ <div
253
+ key={block.id}
254
+ ref={setBlockRef(block.id)}
255
+ onDragOver={(e) => handleDragOverBlock(e, index, blockRefs.current.get(block.id)!)}
256
+ onDrop={(e) => handleDrop(e, index)}
257
+ className="relative mb-4 last:mb-0"
258
+ >
259
+ <BlockWrapper
260
+ block={block}
261
+ onUpdate={(data) => onBlockUpdate(block.id, data, containerId)}
262
+ onDelete={() => onBlockDelete(block.id, containerId)}
263
+ onMoveUp={index > 0 ? () => onBlockMove(block.id, index - 1, containerId) : undefined}
264
+ onMoveDown={index < blocks.length - 1 ? () => onBlockMove(block.id, index + 1, containerId) : undefined}
265
+ />
266
+ </div>
267
+ ))
268
+ )}
269
+ </div>
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Visual Line that shows where the block will land
275
+ */
276
+ function DropIndicator({ position, darkMode }: { position: any; darkMode: boolean }) {
277
+ return (
278
+ <div
279
+ className="absolute z-50 pointer-events-none"
280
+ style={{
281
+ top: `${position.top - 12}px`,
282
+ left: `${position.left}px`,
283
+ width: `${position.width}px`,
284
+ height: '24px',
285
+ }}
286
+ >
287
+ <div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm
288
+ ${darkMode ? 'bg-primary/20 border-primary/40' : 'bg-primary/10 border-primary/30'}`}
289
+ />
290
+ <div className={`absolute top-1/2 left-0 right-0 h-0.5 transform -translate-y-1/2
291
+ ${darkMode ? 'bg-primary' : 'bg-primary'}`}
292
+ />
293
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
294
+ <div className="w-6 h-6 rounded-full flex items-center justify-center bg-primary shadow-lg">
295
+ <div className="w-2 h-2 rounded-full bg-white" />
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ /**
303
+ * Placeholder when the container is empty
304
+ */
305
+ function EmptyState({ isDragging, darkMode, label }: { isDragging: boolean; darkMode: boolean; label: string }) {
306
+ return (
307
+ <div className={`flex flex-col items-center justify-center py-12 px-6 rounded-2xl border border-dashed transition-all
308
+ ${darkMode
309
+ ? isDragging ? 'border-primary/50 bg-primary/10' : 'border-neutral-700 bg-neutral-800/20'
310
+ : isDragging ? 'border-primary/50 bg-primary/5' : 'border-neutral-200 bg-neutral-50/30'
311
+ }`}
312
+ >
313
+ <div className={`p-3 rounded-full mb-3 ${darkMode ? 'bg-neutral-800' : 'bg-neutral-100'}`}>
314
+ <Plus size={20} className={isDragging ? 'text-primary' : 'text-neutral-400'} />
315
+ </div>
316
+ <p className={`text-xs font-black uppercase tracking-wider
317
+ ${isDragging ? 'text-primary' : 'text-neutral-500'}`}>
318
+ {isDragging ? 'Drop Block Here' : label}
319
+ </p>
320
+ </div>
321
+ );
322
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface SaveConfirmationModalProps {
3
+ isOpen: boolean;
4
+ onClose: () => void;
5
+ onConfirm: () => Promise<void>;
6
+ isSaving: boolean;
7
+ postTitle?: string;
8
+ isPublished?: boolean;
9
+ saveAsDraft?: boolean;
10
+ error?: string | null;
11
+ }
12
+ export declare function SaveConfirmationModal({ isOpen, onClose, onConfirm, isSaving, postTitle, isPublished, saveAsDraft, error, }: SaveConfirmationModalProps): React.ReactPortal | null;
13
+ //# sourceMappingURL=SaveConfirmationModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SaveConfirmationModal.d.ts","sourceRoot":"","sources":["SaveConfirmationModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAKnD,MAAM,WAAW,0BAA0B;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,qBAAqB,CAAC,EAClC,MAAM,EACN,OAAO,EACP,SAAS,EACT,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAmB,EACnB,KAAK,GACR,EAAE,0BAA0B,4BA4M5B"}