@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.
- package/package.json +55 -58
- package/src/api/handler.ts +10 -11
- package/src/api/router.ts +1 -4
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCategories.ts +76 -0
- package/src/index.tsx +5 -27
- package/src/init.tsx +0 -9
- package/src/lib/mappers/apiMapper.ts +53 -22
- package/src/registry/BlockRegistry.ts +1 -4
- package/src/state/EditorContext.tsx +39 -33
- package/src/state/types.ts +1 -1
- package/src/types/post.ts +4 -0
- package/src/utils/index.ts +2 -1
- package/src/views/CanvasEditor/BlockWrapper.tsx +7 -8
- package/src/views/CanvasEditor/CanvasEditorView.tsx +208 -794
- package/src/views/CanvasEditor/EditorBody.tsx +317 -127
- package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
- package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +229 -46
- package/src/views/CanvasEditor/components/index.ts +11 -0
- package/src/views/CanvasEditor/hooks/index.ts +10 -0
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
- package/src/views/PostManager/PostActionsMenu.tsx +1 -1
- package/src/views/PostManager/PostCards.tsx +18 -13
- package/src/views/PostManager/PostFilters.tsx +15 -0
- package/src/views/PostManager/PostManagerView.tsx +29 -20
- package/src/views/PostManager/PostStats.tsx +5 -5
- 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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
newHistory.
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
await onSave(stateRef.current);
|
|
190
|
+
await onSave(stateRef.current, heroBlock);
|
|
185
191
|
dispatch({ type: 'MARK_CLEAN' });
|
|
186
192
|
}
|
|
187
193
|
}, [onSave]);
|
package/src/state/types.ts
CHANGED
|
@@ -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 */
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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' }}>
|