@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
@@ -26,7 +26,7 @@ export interface EditorProviderProps {
26
26
  /** Initial state (optional) */
27
27
  initialState?: Partial<EditorState>;
28
28
  /** Callback when save is triggered */
29
- onSave?: (state: EditorState) => Promise<void>;
29
+ onSave?: (state: EditorState, heroBlock?: Block | null) => Promise<void>;
30
30
  /**
31
31
  * Custom blocks from client application
32
32
  * These blocks will be registered in the BlockRegistry on mount
@@ -56,29 +56,19 @@ export function EditorProvider({
56
56
  darkMode = true,
57
57
  backgroundColors
58
58
  }: EditorProviderProps) {
59
- console.log('[EditorProvider] Rendering with darkMode:', darkMode);
60
-
61
59
  // Register core layout blocks on mount
62
60
  useEffect(() => {
63
- console.log('[EditorContext] Registering layout blocks...');
64
61
  registerLayoutBlocks();
65
- const layoutBlocks = blockRegistry.getByCategory('layout');
66
- console.log('[EditorContext] Layout blocks registered:', layoutBlocks.length, layoutBlocks.map(b => b.type));
67
62
  }, []);
68
63
 
69
64
  // Register client blocks on mount
70
65
  useEffect(() => {
71
66
  if (customBlocks && customBlocks.length > 0) {
72
67
  try {
73
- console.log('[EditorContext] Registering custom blocks:', customBlocks.length, customBlocks.map(b => b.type));
74
68
  blockRegistry.registerClientBlocks(customBlocks);
75
- const allBlocks = blockRegistry.getAll();
76
- console.log('[EditorContext] Total blocks after registration:', allBlocks.length, allBlocks.map(b => b.type));
77
69
  } catch (error) {
78
70
  console.error('[EditorContext] Failed to register custom blocks:', error);
79
71
  }
80
- } else {
81
- console.log('[EditorContext] No custom blocks provided');
82
72
  }
83
73
  }, [customBlocks]);
84
74
 
@@ -98,33 +88,50 @@ export function EditorProvider({
98
88
  const MAX_HISTORY = 50; // Limit history to prevent memory issues
99
89
 
100
90
  // Save current state to history after state changes (but not during undo/redo)
91
+ // Debounce history updates to avoid excessive re-renders
92
+ const historyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
93
+
101
94
  useEffect(() => {
102
95
  if (isRestoringRef.current) {
103
96
  isRestoringRef.current = false;
104
97
  return;
105
98
  }
106
99
 
107
- // Save current state to history
108
- setHistory(prev => {
109
- const newHistory = [...prev];
110
- // Remove any future history if we're not at the end
111
- if (historyIndex < newHistory.length - 1) {
112
- newHistory.splice(historyIndex + 1);
113
- }
114
- // Add current state
115
- newHistory.push({ ...state });
116
- // Limit history size
117
- if (newHistory.length > MAX_HISTORY) {
118
- newHistory.shift();
100
+ // Clear existing timeout
101
+ if (historyTimeoutRef.current) {
102
+ clearTimeout(historyTimeoutRef.current);
103
+ }
104
+
105
+ // Debounce history updates to reduce re-renders
106
+ historyTimeoutRef.current = setTimeout(() => {
107
+ // Save current state to history
108
+ setHistory(prev => {
109
+ const newHistory = [...prev];
110
+ // Remove any future history if we're not at the end
111
+ if (historyIndex < newHistory.length - 1) {
112
+ newHistory.splice(historyIndex + 1);
113
+ }
114
+ // Add current state
115
+ newHistory.push({ ...state });
116
+ // Limit history size
117
+ if (newHistory.length > MAX_HISTORY) {
118
+ newHistory.shift();
119
+ return newHistory;
120
+ }
119
121
  return newHistory;
122
+ });
123
+ setHistoryIndex(prev => {
124
+ const newIndex = prev + 1;
125
+ return newIndex >= MAX_HISTORY ? MAX_HISTORY - 1 : newIndex;
126
+ });
127
+ }, 300); // Debounce by 300ms
128
+
129
+ return () => {
130
+ if (historyTimeoutRef.current) {
131
+ clearTimeout(historyTimeoutRef.current);
120
132
  }
121
- return newHistory;
122
- });
123
- setHistoryIndex(prev => {
124
- const newIndex = prev + 1;
125
- return newIndex >= MAX_HISTORY ? MAX_HISTORY - 1 : newIndex;
126
- });
127
- }, [state.blocks, state.title, state.slug, state.seo, state.metadata, state.status]);
133
+ };
134
+ }, [state.blocks, state.title, state.slug, state.seo, state.metadata, state.status, historyIndex]);
128
135
 
129
136
  // Helper: Add a new block (supports nested containers)
130
137
  const addBlock = useCallback((type: string, index?: number, containerId?: string) => {
@@ -176,12 +183,11 @@ export function EditorProvider({
176
183
 
177
184
  // Helper: Save
178
185
  // Uses stateRef to always get the latest state, avoiding stale closure issues
179
- const save = useCallback(async () => {
186
+ const save = useCallback(async (heroBlock?: Block | null) => {
180
187
  if (onSave) {
181
188
  // Use stateRef.current to get the absolute latest state
182
189
  // This ensures we don't have stale closure issues with React state updates
183
- console.log('[EditorContext] save() called with status:', stateRef.current.status);
184
- await onSave(stateRef.current);
190
+ await onSave(stateRef.current, heroBlock);
185
191
  dispatch({ type: 'MARK_CLEAN' });
186
192
  }
187
193
  }, [onSave]);
@@ -118,7 +118,7 @@ export interface EditorContextValue {
118
118
  resetEditor: () => void;
119
119
 
120
120
  /** Save current state (triggers save callback) */
121
- save: () => Promise<void>;
121
+ save: (heroBlock?: Block | null) => Promise<void>;
122
122
 
123
123
  /** Undo last action */
124
124
  undo: () => void;
package/src/types/post.ts CHANGED
@@ -75,6 +75,10 @@ export interface PostMetadata {
75
75
  alt?: string;
76
76
  brightness?: number; // 0-200, 100 = normal
77
77
  blur?: number; // 0-20
78
+ scale?: number; // 0.1-3.0, 1.0 = normal
79
+ positionX?: number; // -100 to 100, 0 = center
80
+ positionY?: number; // -100 to 100, 0 = center
81
+ isCustom?: boolean; // true if edited independently from hero image
78
82
  };
79
83
 
80
84
  /** Categories */
@@ -4,5 +4,6 @@
4
4
  */
5
5
 
6
6
  export * from './client';
7
- export * from '../lib/utils/blockHelpers';
7
+ export * from './blockHelpers';
8
+ export * from './slugify';
8
9
 
@@ -274,7 +274,7 @@ export function BlockWrapper({
274
274
  >
275
275
  {/* Left Margin Controls - Visible on Hover */}
276
276
  <div
277
- className={`absolute -left-16 top-1/2 -translate-y-1/2 flex flex-col gap-2 transition-all duration-200 z-20 ${showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'
277
+ className={`absolute -left-16 top-1/2 -translate-y-1/2 flex flex-col gap-2 transition-all duration-200 ${showControls ? 'opacity-100' : 'opacity-0 pointer-events-none'
278
278
  }`}
279
279
  onMouseEnter={() => setIsControlsHovered(true)}
280
280
  onMouseLeave={() => setIsControlsHovered(false)}
@@ -321,7 +321,7 @@ export function BlockWrapper({
321
321
 
322
322
  {/* Block Header - Positioned above the component */}
323
323
  <div
324
- className={`mb-2 transition-all relative z-[100] ${isHovered || showControls || showSettingsMenu
324
+ className={`mb-2 transition-all relative ${isHovered || showControls || showSettingsMenu
325
325
  ? 'opacity-100 translate-y-0'
326
326
  : 'opacity-0 -translate-y-2 pointer-events-none'
327
327
  }`}
@@ -339,7 +339,7 @@ export function BlockWrapper({
339
339
  </span>
340
340
 
341
341
  {/* Layout Block Settings - Inline in header */}
342
- {block.type === 'section' && (
342
+ {/* {block.type === 'section' && (
343
343
  <div className="flex items-center gap-2 ml-4 flex-1 min-w-0">
344
344
  <select
345
345
  value={(block.data.padding as string) || 'md'}
@@ -363,7 +363,7 @@ export function BlockWrapper({
363
363
  <option value="CREAM">Cream</option>
364
364
  </select>
365
365
  </div>
366
- )}
366
+ )} */}
367
367
 
368
368
  {block.type === 'columns' && (
369
369
  <select
@@ -381,13 +381,13 @@ export function BlockWrapper({
381
381
  </select>
382
382
  )}
383
383
  </div>
384
- <div className="relative shrink-0 z-[200]" ref={settingsMenuRef}>
384
+ <div className="relative shrink-0 z-20" ref={settingsMenuRef}>
385
385
  <button
386
386
  onClick={(e) => {
387
387
  e.stopPropagation();
388
388
  setShowSettingsMenu(!showSettingsMenu);
389
389
  }}
390
- className="p-1 rounded transition-colors text-neutral-400 dark:text-neutral-500 hover:text-neutral-950 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 relative z-[200]"
390
+ className="p-1 rounded transition-colors text-neutral-400 dark:text-neutral-500 hover:text-neutral-950 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 relative z-20"
391
391
  title="Block settings"
392
392
  >
393
393
  <Settings2 size={12} />
@@ -395,7 +395,7 @@ export function BlockWrapper({
395
395
 
396
396
  {/* Settings Dropdown Menu */}
397
397
  {showSettingsMenu && (
398
- <div className="absolute right-0 top-full mt-1 w-40 bg-white dark:bg-neutral-900 border border-neutral-300 dark:border-neutral-700 rounded-lg shadow-xl z-[200] overflow-hidden">
398
+ <div className="absolute right-0 top-full mt-1 w-40 bg-white dark:bg-neutral-900 border border-neutral-300 dark:border-neutral-700 rounded-lg shadow-xl z-20 overflow-hidden">
399
399
  <button
400
400
  onClick={(e) => {
401
401
  e.stopPropagation();
@@ -418,7 +418,6 @@ export function BlockWrapper({
418
418
  ? 'border-primary/60 dark:border-primary/40 bg-primary/5 dark:bg-primary/10'
419
419
  : 'border-neutral-200 dark:border-neutral-700 bg-transparent'
420
420
  }`}
421
- style={{ zIndex: 1 }}
422
421
  >
423
422
  {/* Edit Component - No padding, let the component control its own spacing */}
424
423
  <div className="relative" style={{ userSelect: 'text' }}>