@jhits/plugin-blog 0.0.4 → 0.0.6

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 (36) hide show
  1. package/package.json +55 -58
  2. package/src/api/handler.ts +10 -11
  3. package/src/api/router.ts +1 -4
  4. package/src/hooks/index.ts +1 -0
  5. package/src/hooks/useCategories.ts +76 -0
  6. package/src/index.tsx +5 -27
  7. package/src/init.tsx +0 -9
  8. package/src/lib/mappers/apiMapper.ts +53 -22
  9. package/src/registry/BlockRegistry.ts +1 -4
  10. package/src/state/EditorContext.tsx +39 -33
  11. package/src/state/types.ts +1 -1
  12. package/src/types/post.ts +4 -0
  13. package/src/utils/index.ts +2 -1
  14. package/src/views/CanvasEditor/BlockWrapper.tsx +7 -8
  15. package/src/views/CanvasEditor/CanvasEditorView.tsx +208 -794
  16. package/src/views/CanvasEditor/EditorBody.tsx +317 -127
  17. package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
  18. package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
  19. package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
  20. package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
  21. package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
  22. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  23. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +229 -46
  24. package/src/views/CanvasEditor/components/index.ts +11 -0
  25. package/src/views/CanvasEditor/hooks/index.ts +10 -0
  26. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
  27. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
  28. package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
  29. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
  30. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
  31. package/src/views/PostManager/PostActionsMenu.tsx +1 -1
  32. package/src/views/PostManager/PostCards.tsx +18 -13
  33. package/src/views/PostManager/PostFilters.tsx +15 -0
  34. package/src/views/PostManager/PostManagerView.tsx +29 -20
  35. package/src/views/PostManager/PostStats.tsx +5 -5
  36. package/src/views/PostManager/PostTable.tsx +10 -5
@@ -1,37 +1,15 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useEffect, useRef, useMemo } from 'react';
4
- import {
5
- Globe,
6
- Image as ImageIcon, Library,
7
- LayoutTemplate, Type,
8
- Box,
9
- Search,
10
- AlertTriangle, X
11
- } from 'lucide-react';
3
+ import React, { useState, useEffect, useRef } from 'react';
12
4
  import { useEditor } from '../../state/EditorContext';
13
- import { EditorBody } from './EditorBody';
14
- import { BlockWrapper } from './BlockWrapper';
15
5
  import { EditorHeader } from './EditorHeader';
16
- import { blockRegistry } from '../../registry/BlockRegistry';
17
- import { BlockRenderer } from '../../lib/blocks/BlockRenderer';
18
- import { Block } from '../../types/block';
19
- import { apiToBlogPost, type APIBlogDocument } from '../../lib/mappers/apiMapper';
20
- import {
21
- LibraryItem,
22
- CustomBlockItem,
23
- FeaturedMediaSection,
24
- PrivacySettingsSection
25
- } from './components';
26
-
27
- // Generate a unique block ID
28
- function generateBlockId(): string {
29
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
30
- return crypto.randomUUID();
31
- }
32
- return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
33
- }
34
-
6
+ import { ErrorBanner } from './components/ErrorBanner';
7
+ import { EditorLibrary } from './components/EditorLibrary';
8
+ import { EditorCanvas } from './components/EditorCanvas';
9
+ import { EditorSidebar } from './components/EditorSidebar';
10
+ import { usePostLoader, useHeroBlock, useRegisteredBlocks, useKeyboardShortcuts, useUnsavedChanges } from './hooks';
11
+ import type { Block } from '../../types/block';
12
+ import type { SEOMetadata, PostMetadata } from '../../types/post';
35
13
 
36
14
  export interface CanvasEditorViewProps {
37
15
  postId?: string;
@@ -53,190 +31,87 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
53
31
  const [isSidebarOpen, setSidebarOpen] = useState(true);
54
32
  const [isLibraryOpen, setLibraryOpen] = useState(true);
55
33
  const [isPreviewMode, setIsPreviewMode] = useState(false);
56
- const [isLoadingPost, setIsLoadingPost] = useState(false);
57
34
  const [isSaving, setIsSaving] = useState(false);
58
35
  const [saveError, setSaveError] = useState<string | null>(null);
59
- const titleRef = useRef<HTMLTextAreaElement>(null);
60
-
61
- // Load post when postId (slug) is provided
62
- useEffect(() => {
63
- if (postId && !state.postId) {
64
- const loadPost = async () => {
65
- try {
66
- setIsLoadingPost(true);
67
- const response = await fetch(`/api/plugin-blog/${postId}`);
68
- if (!response.ok) {
69
- throw new Error('Failed to load post');
70
- }
71
- const apiDoc: APIBlogDocument = await response.json();
72
- console.log('[CanvasEditorView] Loaded API document:', {
73
- title: apiDoc.title,
74
- slug: apiDoc.slug,
75
- contentBlocksCount: apiDoc.contentBlocks?.length || 0,
76
- contentBlocks: apiDoc.contentBlocks?.map((b: any) => ({ id: b.id, type: b.type })) || [],
77
- hasContent: !!apiDoc.contentBlocks,
78
- hasLegacyContent: !!apiDoc.content,
79
- });
80
- const blogPost = apiToBlogPost(apiDoc);
81
- console.log('[CanvasEditorView] Converted to BlogPost:', {
82
- id: blogPost.id,
83
- title: blogPost.title,
84
- blocksCount: blogPost.blocks.length,
85
- blocks: blogPost.blocks.map(b => ({ id: b.id, type: b.type })),
86
- });
87
- helpers.loadPost(blogPost);
88
- } catch (error) {
89
- console.error('Failed to load post:', error);
90
- alert('Failed to load post. Please try again.');
91
- } finally {
92
- setIsLoadingPost(false);
93
- }
94
- };
95
- loadPost();
96
- }
97
- }, [postId, state.postId, helpers]);
98
36
 
99
- // Reactive state for registered blocks (updates when registry changes)
100
- const [registeredBlocks, setRegisteredBlocks] = useState(() => {
101
- const initial = blockRegistry.getAll();
102
- console.log('[CanvasEditorView] Initial blocks:', initial.length, initial.map(b => b.type));
103
- return initial;
104
- });
37
+ // Get registered blocks
38
+ const registeredBlocks = useRegisteredBlocks();
39
+
40
+ // Hero block management
41
+ const { heroBlock, setHeroBlock, heroBlockDefinition } = useHeroBlock(state, registeredBlocks);
42
+
43
+ // Post loading
44
+ const { isLoadingPost } = usePostLoader(
45
+ postId,
46
+ state.postId,
47
+ (post) => {
48
+ helpers.loadPost(post);
49
+ // After loading, ensure we're marked as clean
50
+ // Use setTimeout to ensure this runs after the reducer has processed LOAD_POST
51
+ setTimeout(() => {
52
+ dispatch({ type: 'MARK_CLEAN' });
53
+ }, 0);
54
+ },
55
+ () => setHeroBlock(null)
56
+ );
105
57
 
106
- // Watch for registry changes and update state
58
+ // Track if we just loaded a post to prevent marking as dirty during cleanup
59
+ const justLoadedRef = useRef(false);
60
+ const previousIsLoadingRef = useRef<boolean>(false);
61
+ const loadingCleanupTimerRef = useRef<NodeJS.Timeout | null>(null);
62
+
63
+ // Mark when post loading completes and ensure it stays clean after all effects
107
64
  useEffect(() => {
108
- // Check immediately
109
- const checkBlocks = () => {
110
- const currentBlocks = blockRegistry.getAll();
111
- const hasChanged = currentBlocks.length !== registeredBlocks.length ||
112
- currentBlocks.some((b, i) => b.type !== registeredBlocks[i]?.type) ||
113
- registeredBlocks.some((b, i) => b.type !== currentBlocks[i]?.type);
114
-
115
- if (hasChanged) {
116
- console.log('[CanvasEditorView] Blocks updated:', currentBlocks.length, currentBlocks.map(b => `${b.type}(${b.category})`));
117
- setRegisteredBlocks([...currentBlocks]);
118
- }
119
- };
120
-
121
- // Initial check
122
- checkBlocks();
123
-
124
- // Poll for registry changes (blocks are registered asynchronously in useEffect)
125
- // Use a shorter interval initially, then longer
126
- let pollCount = 0;
127
- const interval = setInterval(() => {
128
- pollCount++;
129
- checkBlocks();
130
- // Stop polling after 5 seconds (25 checks at 200ms)
131
- if (pollCount > 25) {
132
- clearInterval(interval);
65
+ // Detect when loading just finished (was loading, now not loading, and we have a postId)
66
+ const loadingJustFinished = previousIsLoadingRef.current && !isLoadingPost && state.postId;
67
+
68
+ if (loadingJustFinished) {
69
+ justLoadedRef.current = true;
70
+
71
+ // Clear any existing cleanup timer
72
+ if (loadingCleanupTimerRef.current) {
73
+ clearTimeout(loadingCleanupTimerRef.current);
133
74
  }
134
- }, 200);
135
-
136
- // Also check after delays to catch initial registrations
137
- const timeouts = [
138
- setTimeout(checkBlocks, 50),
139
- setTimeout(checkBlocks, 100),
140
- setTimeout(checkBlocks, 300),
141
- setTimeout(checkBlocks, 500),
142
- setTimeout(checkBlocks, 1000),
143
- ];
144
-
145
- return () => {
146
- clearInterval(interval);
147
- timeouts.forEach(clearTimeout);
148
- };
149
- }, [registeredBlocks.length]);
150
-
151
-
152
- // Get Hero block definition from registered blocks (REQUIRED)
153
- const heroBlockDefinition = useMemo(() => {
154
- return blockRegistry.get('hero');
155
- }, [registeredBlocks]);
156
-
157
- // Hero block state - separate from content blocks (statically positioned at top)
158
- const [heroBlock, setHeroBlock] = useState<Block | null>(null);
159
-
160
- // Initialize hero block if Hero block definition exists
161
- useEffect(() => {
162
- if (heroBlockDefinition) {
163
- // Get default data from block definition
164
- const heroData = heroBlockDefinition.defaultData || {};
165
-
166
- // Initialize hero block only if it doesn't exist yet
167
- setHeroBlock(prev => {
168
- if (!prev) {
169
- // Initialize hero block from editor state (if editing existing post) or use defaults
170
- const initialData = {
171
- ...heroData,
172
- title: state.title || heroData.title || '',
173
- summary: state.metadata.excerpt || heroData.summary || '',
174
- image: state.metadata.featuredImage ? {
175
- src: state.metadata.featuredImage.src,
176
- alt: state.metadata.featuredImage.alt,
177
- brightness: state.metadata.featuredImage.brightness,
178
- blur: state.metadata.featuredImage.blur,
179
- } : heroData.image,
180
- };
181
- return {
182
- id: generateBlockId(),
183
- type: 'hero',
184
- data: initialData,
185
- };
186
- }
187
- // Keep existing hero block data - let the Edit component manage it
188
- return prev;
75
+
76
+ // Wait for all effects to complete, then ensure we're marked as clean
77
+ // Use multiple animation frames + setTimeout to ensure all effects have run
78
+ requestAnimationFrame(() => {
79
+ requestAnimationFrame(() => {
80
+ loadingCleanupTimerRef.current = setTimeout(() => {
81
+ // Force mark as clean after loading - this ensures cleanup effects don't leave us dirty
82
+ console.log('[CanvasEditorView] Post loading complete - ensuring clean state');
83
+ dispatch({ type: 'MARK_CLEAN' });
84
+ justLoadedRef.current = false;
85
+ loadingCleanupTimerRef.current = null;
86
+ }, 500); // Delay to ensure all effects complete
87
+ });
189
88
  });
190
- } else {
191
- setHeroBlock(null);
192
89
  }
193
- }, [heroBlockDefinition]);
194
-
195
- // Sync hero block with editor state when post is loaded (for existing posts)
196
- useEffect(() => {
197
- if (heroBlock && heroBlockDefinition && state.postId) {
198
- // Only update if the hero block data doesn't match the editor state
199
- // This prevents overwriting user edits with stale data
200
- const currentTitle = (heroBlock.data as any)?.title || '';
201
- const currentSummary = (heroBlock.data as any)?.summary || '';
202
- const currentImage = (heroBlock.data as any)?.image;
203
- const currentCategory = (heroBlock.data as any)?.category || '';
204
-
205
- const stateTitle = state.title || '';
206
- const stateSummary = state.metadata.excerpt || '';
207
- const stateImage = state.metadata.featuredImage;
208
- const stateCategory = state.metadata.categories?.[0] || '';
209
-
210
- // Check if hero block is out of sync with editor state
211
- const titleMismatch = currentTitle !== stateTitle;
212
- const summaryMismatch = currentSummary !== stateSummary;
213
- const imageMismatch = !currentImage && stateImage ||
214
- currentImage?.src !== stateImage?.src ||
215
- currentImage?.alt !== stateImage?.alt ||
216
- currentImage?.brightness !== stateImage?.brightness ||
217
- currentImage?.blur !== stateImage?.blur;
218
- const categoryMismatch = currentCategory !== stateCategory;
219
-
220
- // Only update if there's a mismatch and the editor state has data (post was loaded)
221
- if ((titleMismatch || summaryMismatch || imageMismatch || categoryMismatch) && (stateTitle || stateSummary || stateImage || stateCategory)) {
222
- setHeroBlock({
223
- ...heroBlock,
224
- data: {
225
- ...heroBlock.data,
226
- title: stateTitle || (heroBlock.data as any)?.title || '',
227
- summary: stateSummary || (heroBlock.data as any)?.summary || '',
228
- image: stateImage ? {
229
- src: stateImage.src,
230
- alt: stateImage.alt,
231
- brightness: stateImage.brightness,
232
- blur: stateImage.blur,
233
- } : (heroBlock.data as any)?.image,
234
- category: stateCategory || (heroBlock.data as any)?.category || '',
235
- },
236
- });
90
+
91
+ // Update ref
92
+ previousIsLoadingRef.current = isLoadingPost;
93
+
94
+ return () => {
95
+ if (loadingCleanupTimerRef.current) {
96
+ clearTimeout(loadingCleanupTimerRef.current);
97
+ loadingCleanupTimerRef.current = null;
237
98
  }
238
- }
239
- }, [state.postId, state.title, state.metadata.excerpt, state.metadata.featuredImage, state.metadata.categories, heroBlockDefinition]);
99
+ };
100
+ }, [isLoadingPost, state.postId, dispatch]);
101
+
102
+ // Keyboard shortcuts
103
+ useKeyboardShortcuts(state, dispatch, canUndo, canRedo, helpers.undo, helpers.redo);
104
+
105
+ // Unsaved changes warning and auto-save
106
+ const { autoSaveEnabled, setAutoSaveEnabled, countdown, saveStatus } = useUnsavedChanges({
107
+ state,
108
+ isDirty: state.isDirty,
109
+ onSave: async () => {
110
+ await handleSave(false); // Auto-save as draft
111
+ },
112
+ heroBlock,
113
+ postId: state.postId,
114
+ });
240
115
 
241
116
  // Listen for hero title updates from HeroBlock (if it dispatches events)
242
117
  useEffect(() => {
@@ -248,192 +123,126 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
248
123
  }, [dispatch]);
249
124
 
250
125
  // Remove any hero blocks from the content blocks array
126
+ // Note: This effect will mark as dirty, but the loading cleanup effect will fix it
251
127
  useEffect(() => {
252
128
  const heroBlocksInContent = state.blocks.filter(b => b.type === 'hero');
253
129
  if (heroBlocksInContent.length > 0) {
254
130
  heroBlocksInContent.forEach(block => {
255
131
  dispatch({ type: 'DELETE_BLOCK', payload: { id: block.id } });
256
132
  });
133
+ // Don't mark as clean here - let the loading cleanup effect handle it
134
+ // This ensures we wait for all effects to complete before marking clean
257
135
  }
258
136
  }, [state.blocks, dispatch]);
259
137
 
260
138
  // Filter out hero blocks from content blocks
261
139
  const contentBlocks = state.blocks.filter(b => b.type !== 'hero');
262
140
 
263
- // Get all registered blocks from state (excluding Hero block from sidebar)
264
- const allBlocks = registeredBlocks.filter(block => block.type !== 'hero');
265
- const textBlocks = allBlocks.filter(block => block.category === 'text');
266
- const customBlocks = allBlocks.filter(block => block.category === 'custom');
267
- const mediaBlocks = allBlocks.filter(block => block.category === 'media');
268
- const layoutBlocks = allBlocks.filter(block => block.category === 'layout');
269
-
270
141
  // Handler to add block at the bottom when clicking (not dragging)
271
142
  const handleAddBlockAtBottom = (blockType: string) => {
272
143
  // Add at the end of content blocks (excluding hero)
273
144
  helpers.addBlock(blockType, contentBlocks.length, undefined);
274
145
  };
275
146
 
276
- // Handle Title Auto-resize
277
- useEffect(() => {
278
- if (titleRef.current) {
279
- titleRef.current.style.height = 'auto';
280
- titleRef.current.style.height = `${titleRef.current.scrollHeight}px`;
281
- }
282
- }, [state.title]);
283
-
284
- // Keyboard shortcuts: Ctrl+V to paste, Ctrl+Z to undo, Ctrl+Shift+Z/Ctrl+Y to redo
285
- useEffect(() => {
286
- const handleKeyDown = (e: KeyboardEvent) => {
287
- // Don't handle shortcuts if user is typing in an input/textarea/contentEditable
288
- const target = e.target as HTMLElement;
289
- const isEditableElement = target.tagName === 'INPUT' ||
290
- target.tagName === 'TEXTAREA' ||
291
- target.isContentEditable ||
292
- target.closest('input, textarea, [contenteditable="true"]');
293
-
294
- // Check for Ctrl+Z / Cmd+Z (Undo)
295
- if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
296
- if (!isEditableElement) {
297
- e.preventDefault();
298
- e.stopPropagation();
299
- if (canUndo) {
300
- helpers.undo();
301
- }
302
- return;
303
- }
147
+ // Handle save
148
+ const handleSave = async (publish?: boolean) => {
149
+ setIsSaving(true);
150
+ setSaveError(null);
151
+ try {
152
+ // Status should already be set in EditorHeader, but verify and log
153
+ console.log('[CanvasEditorView] onSave called with publish:', publish, 'current status:', state.status);
154
+
155
+ // Double-check status is set correctly before saving
156
+ if (publish === true && state.status !== 'published') {
157
+ console.warn('[CanvasEditorView] Status mismatch! Setting to published...');
158
+ dispatch({ type: 'SET_STATUS', payload: 'published' });
159
+ await new Promise(resolve => setTimeout(resolve, 100));
160
+ } else if (publish === false && state.status !== 'draft') {
161
+ console.warn('[CanvasEditorView] Status mismatch! Setting to draft...');
162
+ dispatch({ type: 'SET_STATUS', payload: 'draft' });
163
+ await new Promise(resolve => setTimeout(resolve, 100));
304
164
  }
305
165
 
306
- // Check for Ctrl+Shift+Z / Cmd+Shift+Z or Ctrl+Y / Cmd+Y (Redo)
307
- if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') || ((e.ctrlKey || e.metaKey) && e.key === 'y')) {
308
- if (!isEditableElement) {
309
- e.preventDefault();
310
- e.stopPropagation();
311
- if (canRedo) {
312
- helpers.redo();
313
- }
314
- return;
315
- }
166
+ console.log('[CanvasEditorView] Final status before save:', state.status);
167
+
168
+ // Pass hero block to save function so it can be included in the saved data
169
+ await helpers.save(heroBlock);
170
+ setIsSaving(false);
171
+ } catch (error: any) {
172
+ console.error('[CanvasEditorView] Save error:', error);
173
+ // Extract and format user-friendly error message
174
+ let errorMessage = error.message || 'Failed to save post';
175
+
176
+ // Make error messages more user-friendly
177
+ if (errorMessage.includes('Missing required fields')) {
178
+ errorMessage = errorMessage.replace('Missing required fields for publishing:', 'To publish, please fill in:');
179
+ } else if (errorMessage.includes('All required fields')) {
180
+ errorMessage = 'To publish, please fill in all required fields: summary, featured image, category, and content.';
181
+ } else if (errorMessage.includes('Unauthorized')) {
182
+ errorMessage = 'You are not authorized to save this post. Please log in again.';
183
+ } else if (errorMessage.includes('Failed to save')) {
184
+ errorMessage = 'Unable to save the post. Please check your connection and try again.';
316
185
  }
317
186
 
318
- // Check for Ctrl+V (Windows/Linux) or Cmd+V (Mac)
319
- if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
320
- // Don't paste if user is typing in an input/textarea/contentEditable
321
- const target = e.target as HTMLElement;
322
- const isEditableElement = target.tagName === 'INPUT' ||
323
- target.tagName === 'TEXTAREA' ||
324
- target.isContentEditable ||
325
- target.closest('input, textarea, [contenteditable="true"]');
326
-
327
- if (isEditableElement) {
328
- return; // Let the browser handle paste in editable elements
329
- }
330
-
331
- // Check if there's a copied block
332
- if (typeof window !== 'undefined') {
333
- const copiedBlockJson = localStorage.getItem('__BLOG_EDITOR_COPIED_BLOCK__');
334
- if (copiedBlockJson) {
335
- try {
336
- e.preventDefault();
337
- e.stopPropagation();
338
-
339
- const copiedBlock = JSON.parse(copiedBlockJson) as Block;
340
-
341
- // Generate a unique block ID
342
- const generateBlockId = (): string => {
343
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
344
- return crypto.randomUUID();
345
- }
346
- return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
347
- };
348
-
349
- // Clone a block with new IDs (recursive for nested blocks)
350
- const cloneBlock = (blockToClone: Block): Block => {
351
- const cloned: Block = {
352
- ...blockToClone,
353
- id: generateBlockId(),
354
- data: { ...blockToClone.data },
355
- meta: blockToClone.meta ? { ...blockToClone.meta } : undefined,
356
- };
357
-
358
- // Handle children if they exist
359
- if (blockToClone.children) {
360
- if (Array.isArray(blockToClone.children) && blockToClone.children.length > 0) {
361
- if (typeof blockToClone.children[0] === 'object') {
362
- cloned.children = (blockToClone.children as Block[]).map(cloneBlock);
363
- } else {
364
- // If children are IDs, find and clone the actual blocks
365
- const allBlocks = state.blocks;
366
- const childIds = blockToClone.children as string[];
367
- const childBlocks = childIds
368
- .map((childId: string) => allBlocks.find(b => b.id === childId))
369
- .filter((b): b is Block => b !== undefined);
370
- cloned.children = childBlocks.map(cloneBlock);
371
- }
372
- }
373
- }
374
-
375
- return cloned;
376
- };
377
-
378
- const pastedBlock = cloneBlock(copiedBlock);
379
-
380
- // Find where to paste - use hovered block or selected block, or paste at end
381
- const hoveredBlockId = (window as any).__BLOG_EDITOR_HOVERED_BLOCK_ID__;
382
- const targetBlockId = hoveredBlockId || state.selectedBlockId;
383
-
384
- let pasteIndex: number | undefined;
385
- if (targetBlockId) {
386
- const targetIndex = state.blocks.findIndex(b => b.id === targetBlockId);
387
- if (targetIndex !== -1) {
388
- pasteIndex = targetIndex + 1;
389
- }
390
- }
187
+ setSaveError(errorMessage);
188
+ setIsSaving(false); // Always reset saving state on error
189
+ throw error; // Re-throw so EditorHeader can handle it
190
+ }
191
+ };
391
192
 
392
- // Dispatch ADD_BLOCK with the full block structure
393
- dispatch({
394
- type: 'ADD_BLOCK',
395
- payload: {
396
- block: pastedBlock,
397
- index: pasteIndex,
398
- containerId: undefined
399
- }
400
- });
401
- } catch (error) {
402
- console.error('Failed to paste block:', error);
403
- }
404
- }
193
+ // Handle hero block update
194
+ const handleHeroBlockUpdate = (data: Partial<Block['data']>) => {
195
+ if (!heroBlock) return;
196
+
197
+ setHeroBlock({
198
+ ...heroBlock,
199
+ data: { ...heroBlock.data, ...data },
200
+ });
201
+
202
+ // Sync title to editor state
203
+ if (data.title !== undefined && typeof data.title === 'string') {
204
+ dispatch({ type: 'SET_TITLE', payload: data.title });
205
+ }
206
+
207
+ // Sync summary to editor state metadata
208
+ if (data.summary !== undefined && typeof data.summary === 'string') {
209
+ dispatch({
210
+ type: 'SET_METADATA',
211
+ payload: { excerpt: data.summary }
212
+ });
213
+ }
214
+
215
+ // Hero image and featured image are completely independent
216
+ // Do NOT sync hero image to featured image
217
+ // The featured image is a separate thumbnail that the client adjusts independently
218
+
219
+ // Sync category to editor state metadata
220
+ if (data.category !== undefined && typeof data.category === 'string') {
221
+ dispatch({
222
+ type: 'SET_METADATA',
223
+ payload: {
224
+ categories: data.category.trim() ? [data.category.trim()] : []
405
225
  }
406
- }
407
- };
226
+ });
227
+ }
228
+ };
408
229
 
409
- window.addEventListener('keydown', handleKeyDown);
410
- return () => {
411
- window.removeEventListener('keydown', handleKeyDown);
412
- };
413
- }, [state.blocks, state.selectedBlockId, dispatch]);
230
+ // Handle hero block delete/reset
231
+ const handleHeroBlockDelete = () => {
232
+ if (!heroBlock || !heroBlockDefinition) return;
233
+ const defaultData = heroBlockDefinition.defaultData || {};
234
+ setHeroBlock({
235
+ ...heroBlock,
236
+ data: { ...defaultData },
237
+ });
238
+ };
414
239
 
415
240
  return (
416
241
  <div className="h-full rounded-[2.5rem] w-full bg-dashboard-card text-dashboard-text flex flex-col font-sans transition-colors duration-300 overflow-hidden relative">
417
-
418
242
  <main className="flex flex-1 flex-col relative min-h-0">
419
243
  {/* Error Banner */}
420
- {saveError && (
421
- <div className="bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 px-6 py-3 flex items-center justify-between">
422
- <div className="flex items-center gap-3 flex-1">
423
- <AlertTriangle className="text-red-600 dark:text-red-400 flex-shrink-0" size={20} />
424
- <p className="text-red-800 dark:text-red-300 text-sm font-medium">
425
- {saveError}
426
- </p>
427
- </div>
428
- <button
429
- onClick={() => setSaveError(null)}
430
- className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200 transition-colors"
431
- aria-label="Dismiss error"
432
- >
433
- <X size={18} />
434
- </button>
435
- </div>
436
- )}
244
+ <ErrorBanner error={saveError} onDismiss={() => setSaveError(null)} />
245
+
437
246
  <EditorHeader
438
247
  isLibraryOpen={isLibraryOpen}
439
248
  onLibraryToggle={() => setLibraryOpen(!isLibraryOpen)}
@@ -442,48 +251,7 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
442
251
  isSidebarOpen={isSidebarOpen}
443
252
  onSidebarToggle={() => setSidebarOpen(!isSidebarOpen)}
444
253
  isSaving={isSaving}
445
- onSave={async (publish?: boolean) => {
446
- setIsSaving(true);
447
- setSaveError(null);
448
- try {
449
- // Status should already be set in EditorHeader, but verify and log
450
- console.log('[CanvasEditorView] onSave called with publish:', publish, 'current status:', state.status);
451
-
452
- // Double-check status is set correctly before saving
453
- if (publish === true && state.status !== 'published') {
454
- console.warn('[CanvasEditorView] Status mismatch! Setting to published...');
455
- dispatch({ type: 'SET_STATUS', payload: 'published' });
456
- await new Promise(resolve => setTimeout(resolve, 100));
457
- } else if (publish === false && state.status !== 'draft') {
458
- console.warn('[CanvasEditorView] Status mismatch! Setting to draft...');
459
- dispatch({ type: 'SET_STATUS', payload: 'draft' });
460
- await new Promise(resolve => setTimeout(resolve, 100));
461
- }
462
-
463
- console.log('[CanvasEditorView] Final status before save:', state.status);
464
- await helpers.save();
465
- setIsSaving(false);
466
- } catch (error: any) {
467
- console.error('[CanvasEditorView] Save error:', error);
468
- // Extract and format user-friendly error message
469
- let errorMessage = error.message || 'Failed to save post';
470
-
471
- // Make error messages more user-friendly
472
- if (errorMessage.includes('Missing required fields')) {
473
- errorMessage = errorMessage.replace('Missing required fields for publishing:', 'To publish, please fill in:');
474
- } else if (errorMessage.includes('All required fields')) {
475
- errorMessage = 'To publish, please fill in all required fields: summary, featured image, category, and content.';
476
- } else if (errorMessage.includes('Unauthorized')) {
477
- errorMessage = 'You are not authorized to save this post. Please log in again.';
478
- } else if (errorMessage.includes('Failed to save')) {
479
- errorMessage = 'Unable to save the post. Please check your connection and try again.';
480
- }
481
-
482
- setSaveError(errorMessage);
483
- setIsSaving(false); // Always reset saving state on error
484
- throw error; // Re-throw so EditorHeader can handle it
485
- }
486
- }}
254
+ onSave={handleSave}
487
255
  onSaveError={(error) => {
488
256
  // Format error message for display
489
257
  if (error) {
@@ -496,258 +264,48 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
496
264
  setSaveError(null);
497
265
  }
498
266
  }}
267
+ autoSaveEnabled={autoSaveEnabled}
268
+ onAutoSaveToggle={setAutoSaveEnabled}
269
+ isDirty={state.isDirty}
270
+ autoSaveCountdown={countdown}
271
+ autoSaveStatus={saveStatus}
499
272
  />
500
273
 
501
274
  {/* Editor Content Wrapper */}
502
275
  <div className="flex flex-1 relative overflow-hidden min-h-0 flex-nowrap">
503
-
504
276
  {/* LEFT SIDEBAR: COMPONENT LIBRARY */}
505
277
  {!isPreviewMode && (
506
278
  <aside
507
279
  className={`transition-all duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] border-r border-dashboard-border bg-dashboard-sidebar overflow-y-auto overflow-x-hidden h-full ${isLibraryOpen ? 'w-72' : 'w-0 opacity-0 pointer-events-none'
508
280
  }`}
509
281
  >
510
- <div className="p-6 w-72 min-w-0 max-w-full">
511
- {/* Text Blocks */}
512
- {textBlocks.length > 0 && (
513
- <div className="mb-10">
514
- <h3 className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6">Text</h3>
515
- <div className="grid grid-cols-2 gap-3">
516
- {textBlocks.map((block) => {
517
- const IconComponent = block.icon || block.components.Icon || Type;
518
- return (
519
- <LibraryItem
520
- key={block.type}
521
- icon={<IconComponent size={16} />}
522
- label={block.name}
523
- blockType={block.type}
524
- description={block.description}
525
- onAddBlock={handleAddBlockAtBottom}
526
- />
527
- );
528
- })}
529
- </div>
530
- </div>
531
- )}
532
-
533
- {/* Media Blocks */}
534
- {mediaBlocks.length > 0 && (
535
- <div className="mb-10">
536
- <h3 className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6">Media</h3>
537
- <div className="grid grid-cols-2 gap-3">
538
- {mediaBlocks.map((block) => {
539
- const IconComponent = block.icon || block.components.Icon || ImageIcon;
540
- return (
541
- <LibraryItem
542
- key={block.type}
543
- icon={<IconComponent size={16} />}
544
- label={block.name}
545
- blockType={block.type}
546
- description={block.description}
547
- onAddBlock={handleAddBlockAtBottom}
548
- />
549
- );
550
- })}
551
- </div>
552
- </div>
553
- )}
554
-
555
- {/* Layout Blocks */}
556
- {layoutBlocks.length > 0 && (
557
- <div className="mb-10">
558
- <h3 className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6">Layout</h3>
559
- <div className="grid grid-cols-2 gap-3">
560
- {layoutBlocks.map((block) => {
561
- const IconComponent = block.icon || block.components.Icon || LayoutTemplate;
562
- return (
563
- <LibraryItem
564
- key={block.type}
565
- icon={<IconComponent size={16} />}
566
- label={block.name}
567
- blockType={block.type}
568
- description={block.description}
569
- onAddBlock={handleAddBlockAtBottom}
570
- />
571
- );
572
- })}
573
- </div>
574
- </div>
575
- )}
576
-
577
- {/* Custom Blocks */}
578
- {customBlocks.length > 0 && (
579
- <div>
580
- <h3 className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black mb-6">Custom Blocks</h3>
581
- <div className="space-y-3">
582
- {customBlocks.map((block) => {
583
- const IconComponent = block.icon || block.components.Icon || Box;
584
- return (
585
- <CustomBlockItem
586
- key={block.type}
587
- blockType={block.type}
588
- name={block.name}
589
- description={block.description}
590
- icon={<IconComponent size={14} />}
591
- onAddBlock={handleAddBlockAtBottom}
592
- />
593
- );
594
- })}
595
- </div>
596
- </div>
597
- )}
598
-
599
- {/* Empty State */}
600
- {allBlocks.length === 0 && (
601
- <div className="text-center py-12">
602
- <Library size={32} className="mx-auto text-neutral-300 dark:text-neutral-700 mb-4" />
603
- <p className="text-xs text-neutral-500 dark:text-neutral-400">
604
- No blocks registered yet.
605
- </p>
606
- </div>
607
- )}
608
- </div>
282
+ <EditorLibrary
283
+ registeredBlocks={registeredBlocks}
284
+ onAddBlock={handleAddBlockAtBottom}
285
+ />
609
286
  </aside>
610
287
  )}
611
288
 
612
289
  {/* CENTER: THE WRITING CANVAS */}
613
- <div
614
- className="flex-1 overflow-y-auto overflow-x-hidden pb-40 px-6 custom-scrollbar selection:bg-primary/20 dark:selection:bg-primary/30 min-h-0"
615
- style={{
616
- backgroundColor: effectiveBackgroundColors
617
- ? (effectiveDarkMode && effectiveBackgroundColors.dark
618
- ? effectiveBackgroundColors.dark
619
- : effectiveBackgroundColors.light)
620
- : undefined,
621
- }}
622
- >
623
- <div className={`mx-auto transition-all duration-500 max-w-7xl w-full`}>
624
- {/* Hero Block - Only show editable version when NOT in preview mode */}
625
- {!isPreviewMode && heroBlockDefinition && heroBlock && (
626
- <div className="mb-12">
627
- <BlockWrapper
628
- block={heroBlock}
629
- onUpdate={(data) => {
630
- setHeroBlock({
631
- ...heroBlock,
632
- data: { ...heroBlock.data, ...data },
633
- });
634
- // Sync title to editor state
635
- if (data.title !== undefined && typeof data.title === 'string') {
636
- dispatch({ type: 'SET_TITLE', payload: data.title });
637
- }
638
- // Sync summary to editor state metadata
639
- if (data.summary !== undefined && typeof data.summary === 'string') {
640
- dispatch({
641
- type: 'SET_METADATA',
642
- payload: { excerpt: data.summary }
643
- });
644
- }
645
- // Sync image to editor state metadata
646
- if (data.image !== undefined) {
647
- const imageData = data.image as any;
648
- dispatch({
649
- type: 'SET_METADATA',
650
- payload: {
651
- featuredImage: imageData ? {
652
- src: imageData.src || '',
653
- alt: imageData.alt || '',
654
- brightness: imageData.brightness ?? 100,
655
- blur: imageData.blur ?? 0,
656
- } : undefined
657
- }
658
- });
659
- }
660
- // Sync category to editor state metadata
661
- if (data.category !== undefined && typeof data.category === 'string') {
662
- dispatch({
663
- type: 'SET_METADATA',
664
- payload: {
665
- categories: data.category.trim() ? [data.category.trim()] : []
666
- }
667
- });
668
- }
669
- }}
670
- onDelete={() => {
671
- const defaultData = heroBlockDefinition.defaultData || {};
672
- setHeroBlock({
673
- ...heroBlock,
674
- data: { ...defaultData },
675
- });
676
- }}
677
- onMoveUp={() => { }}
678
- onMoveDown={() => { }}
679
- allBlocks={[heroBlock]}
680
- />
681
- </div>
682
- )}
683
-
684
- {isPreviewMode ? (
685
- <div className="space-y-8">
686
- {heroBlockDefinition && heroBlock && (
687
- <BlockRenderer
688
- block={heroBlock}
689
- context={{ siteId, locale }}
690
- />
691
- )}
692
-
693
- {!heroBlockDefinition && state.title && (
694
- <h1 className="text-5xl font-serif font-medium text-neutral-950 dark:text-white leading-tight mb-12">
695
- {state.title}
696
- </h1>
697
- )}
698
-
699
- {contentBlocks.length > 0 ? (
700
- <div className="space-y-8">
701
- {contentBlocks.map((block) => (
702
- <BlockRenderer
703
- key={block.id}
704
- block={block}
705
- context={{ siteId, locale }}
706
- />
707
- ))}
708
- </div>
709
- ) : (
710
- <div className="text-center py-20 text-neutral-400 dark:text-neutral-500">
711
- <p className="text-sm">No content blocks yet. Switch to Edit mode to add blocks.</p>
712
- </div>
713
- )}
714
- </div>
715
- ) : (
716
- <>
717
- {!heroBlockDefinition && (
718
- <div className="mb-12">
719
- <textarea
720
- ref={titleRef}
721
- rows={1}
722
- value={state.title}
723
- onChange={(e) => dispatch({ type: 'SET_TITLE', payload: e.target.value })}
724
- placeholder="The title of your story..."
725
- className="w-full bg-transparent border-none outline-none text-5xl font-serif font-medium placeholder:text-neutral-500 dark:placeholder:text-neutral-500 resize-none leading-tight transition-colors duration-300 text-neutral-950 dark:text-white"
726
- />
727
- </div>
728
- )}
729
-
730
- <EditorBody
731
- blocks={contentBlocks}
732
- darkMode={effectiveDarkMode}
733
- backgroundColors={effectiveBackgroundColors}
734
- onBlockAdd={(type, index, containerId) => {
735
- helpers.addBlock(type, index, containerId);
736
- }}
737
- onBlockUpdate={(id, data) => {
738
- helpers.updateBlock(id, data);
739
- }}
740
- onBlockDelete={(id) => {
741
- helpers.deleteBlock(id);
742
- }}
743
- onBlockMove={(id, newIndex, containerId) => {
744
- helpers.moveBlock(id, newIndex, containerId);
745
- }}
746
- />
747
- </>
748
- )}
749
- </div>
750
- </div>
290
+ <EditorCanvas
291
+ isPreviewMode={isPreviewMode}
292
+ heroBlock={heroBlock}
293
+ heroBlockDefinition={heroBlockDefinition}
294
+ contentBlocks={contentBlocks}
295
+ title={state.title}
296
+ siteId={siteId}
297
+ locale={locale}
298
+ darkMode={effectiveDarkMode}
299
+ backgroundColors={effectiveBackgroundColors}
300
+ featuredImage={state.metadata.featuredImage}
301
+ onTitleChange={(title: string) => dispatch({ type: 'SET_TITLE', payload: title })}
302
+ onHeroBlockUpdate={handleHeroBlockUpdate}
303
+ onHeroBlockDelete={handleHeroBlockDelete}
304
+ onBlockAdd={(type: string, index: number, containerId?: string) => helpers.addBlock(type, index, containerId)}
305
+ onBlockUpdate={(id: string, data: Partial<Block['data']>) => helpers.updateBlock(id, data)}
306
+ onBlockDelete={(id: string) => helpers.deleteBlock(id)}
307
+ onBlockMove={(id: string, newIndex: number, containerId?: string) => helpers.moveBlock(id, newIndex, containerId)}
308
+ />
751
309
 
752
310
  {/* RIGHT SIDEBAR: THE "DESK" (SETTINGS) */}
753
311
  {!isPreviewMode && (
@@ -755,158 +313,15 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
755
313
  className={`transition-all duration-500 ease-[cubic-bezier(0.4,0,0.2,1)] border-l border-dashboard-border bg-dashboard-sidebar overflow-y-auto overflow-x-hidden h-full ${isSidebarOpen ? 'w-80' : 'w-0 opacity-0 pointer-events-none'
756
314
  }`}
757
315
  >
758
- <div className="p-8 w-80 min-w-0 max-w-full space-y-12 overflow-y-auto max-h-full">
759
- {/* SEO Section */}
760
- <section>
761
- <div className="flex items-center gap-3 mb-6">
762
- <Search size={14} className="text-neutral-500 dark:text-neutral-400" />
763
- <label className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black">
764
- SEO Settings
765
- </label>
766
- </div>
767
- <div className="space-y-4">
768
- <div>
769
- <label className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2">
770
- SEO Title
771
- </label>
772
- <input
773
- type="text"
774
- value={state.seo.title || ''}
775
- onChange={(e) => dispatch({ type: 'SET_SEO', payload: { title: e.target.value } })}
776
- placeholder="SEO title (defaults to post title)"
777
- className="w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text"
778
- />
779
- <p className="text-[9px] text-neutral-400 dark:text-neutral-500 mt-1">
780
- {state.seo.title?.length || 0} / 60 characters
781
- </p>
782
- </div>
783
- <div>
784
- <label className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2">
785
- Meta Description
786
- </label>
787
- <textarea
788
- value={state.seo.description || ''}
789
- onChange={(e) => dispatch({ type: 'SET_SEO', payload: { description: e.target.value } })}
790
- placeholder="Brief description for search engines"
791
- rows={3}
792
- className="w-full px-3 py-2 text-xs bg-white dark:bg-neutral-900/50 border border-neutral-300 dark:border-neutral-700 rounded-lg outline-none focus:border-primary transition-all dark:text-neutral-100 resize-none"
793
- />
794
- <p className="text-[9px] text-neutral-400 dark:text-neutral-500 mt-1">
795
- {state.seo.description?.length || 0} / 160 characters
796
- </p>
797
- </div>
798
- <div>
799
- <label className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2">
800
- Keywords (comma-separated)
801
- </label>
802
- <input
803
- type="text"
804
- value={state.seo.keywords?.join(', ') || ''}
805
- onChange={(e) => {
806
- const keywords = e.target.value.split(',').map(k => k.trim()).filter(k => k);
807
- dispatch({ type: 'SET_SEO', payload: { keywords } });
808
- }}
809
- placeholder="keyword1, keyword2, keyword3"
810
- className="w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text"
811
- />
812
- </div>
813
- <div>
814
- <label className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2">
815
- Open Graph Image URL
816
- </label>
817
- <input
818
- type="url"
819
- value={state.seo.ogImage || ''}
820
- onChange={(e) => dispatch({ type: 'SET_SEO', payload: { ogImage: e.target.value } })}
821
- placeholder="https://example.com/image.jpg"
822
- className="w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text"
823
- />
824
- </div>
825
- </div>
826
- </section>
827
-
828
- {/* Publishing Section */}
829
- <section className="pt-8 border-t border-neutral-200 dark:border-neutral-800">
830
- <div className="flex items-center gap-3 mb-6">
831
- <Globe size={14} className="text-neutral-500 dark:text-neutral-400" />
832
- <label className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black">
833
- Publishing
834
- </label>
835
- </div>
836
- <div className="bg-dashboard-bg p-5 rounded-2xl border border-dashboard-border">
837
- <span className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-3">Slug / Permalink</span>
838
- <div className="text-xs font-mono break-all text-neutral-600 dark:text-neutral-400 leading-relaxed">
839
- /blog/<span className="text-neutral-950 dark:text-white bg-amber-50 dark:bg-amber-900/20 px-1 rounded">{state.slug || 'untitled-post'}</span>
840
- </div>
841
- </div>
842
- </section>
843
-
844
- {/* Category Section */}
845
- <section className="pt-8 border-t border-neutral-200 dark:border-neutral-800">
846
- <div className="flex items-center gap-3 mb-6">
847
- <Box size={14} className="text-neutral-500 dark:text-neutral-400" />
848
- <label className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black">
849
- Category
850
- </label>
851
- </div>
852
- <div className="space-y-4">
853
- <div>
854
- <label className="text-[10px] text-neutral-500 dark:text-neutral-400 uppercase font-bold block mb-2">
855
- Category
856
- </label>
857
- <input
858
- type="text"
859
- value={state.metadata.categories?.[0] || ''}
860
- onChange={(e) => {
861
- const category = e.target.value.trim();
862
- dispatch({
863
- type: 'SET_METADATA',
864
- payload: {
865
- categories: category ? [category] : []
866
- }
867
- });
868
- }}
869
- placeholder="Enter category (required for publishing)"
870
- className="w-full px-3 py-2 text-xs bg-dashboard-card border border-dashboard-border rounded-lg outline-none focus:border-primary transition-all text-dashboard-text"
871
- />
872
- <p className="text-[9px] text-neutral-400 dark:text-neutral-500 mt-1">
873
- {state.metadata.categories?.[0] ? 'Category set' : 'No category set'}
874
- </p>
875
- </div>
876
- </div>
877
- </section>
878
-
879
- {/* Featured Media Section */}
880
- <FeaturedMediaSection
881
- featuredImage={state.metadata.featuredImage}
882
- onUpdate={(image) => dispatch({ type: 'SET_METADATA', payload: { featuredImage: image } })}
883
- />
884
-
885
- {/* Privacy Settings Section */}
886
- <PrivacySettingsSection
887
- privacy={state.metadata.privacy}
888
- onUpdate={(privacy) => dispatch({ type: 'SET_METADATA', payload: { privacy } })}
889
- />
890
-
891
- {/* Post Status Section */}
892
- <section className="pt-8 border-t border-neutral-200 dark:border-neutral-800">
893
- <div className="flex items-center justify-between mb-4">
894
- <label className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black">
895
- Post Status
896
- </label>
897
- <span className="text-[10px] font-black text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 px-2.5 py-1 rounded-full uppercase tracking-tighter">
898
- {state.status}
899
- </span>
900
- </div>
901
- <p className="text-[11px] text-neutral-500 dark:text-neutral-400 leading-relaxed italic">
902
- {state.status === 'draft'
903
- ? 'This post is private. Only you can see it until you hit publish.'
904
- : state.status === 'published'
905
- ? 'This post is live and visible to everyone.'
906
- : 'This post is scheduled for publication.'}
907
- </p>
908
- </section>
909
- </div>
316
+ <EditorSidebar
317
+ slug={state.slug}
318
+ seo={state.seo}
319
+ metadata={state.metadata}
320
+ heroBlock={heroBlock}
321
+ status={state.status}
322
+ onSEOUpdate={(seo: Partial<SEOMetadata>) => dispatch({ type: 'SET_SEO', payload: seo })}
323
+ onMetadataUpdate={(metadata: Partial<PostMetadata>) => dispatch({ type: 'SET_METADATA', payload: metadata })}
324
+ />
910
325
  </aside>
911
326
  )}
912
327
  </div>
@@ -914,4 +329,3 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
914
329
  </div>
915
330
  );
916
331
  }
917
-