@jhits/plugin-blog 0.0.5 → 0.0.7
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 +16 -16
- package/src/api/config-handler.ts +76 -0
- package/src/api/handler.ts +4 -4
- package/src/api/router.ts +17 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCategories.ts +76 -0
- package/src/index.tsx +8 -27
- package/src/init.tsx +0 -9
- package/src/lib/config-storage.ts +65 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +177 -13
- package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +6 -1
- 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/index.ts +2 -0
- package/src/types/post.ts +4 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +87 -24
- package/src/views/CanvasEditor/CanvasEditorView.tsx +214 -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 +260 -49
- 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/PostCards.tsx +18 -13
- package/src/views/PostManager/PostFilters.tsx +15 -0
- package/src/views/PostManager/PostManagerView.tsx +21 -15
- package/src/views/PostManager/PostTable.tsx +7 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
|
-
import { ArrowLeft, Library, Settings2 } from 'lucide-react';
|
|
4
|
+
import { ArrowLeft, Library, Settings2, Save, Clock, Edit, Eye } from 'lucide-react';
|
|
5
5
|
import { useEditor } from '../../state/EditorContext';
|
|
6
6
|
import { SaveConfirmationModal } from './SaveConfirmationModal';
|
|
7
7
|
|
|
@@ -15,6 +15,11 @@ export interface EditorHeaderProps {
|
|
|
15
15
|
isSaving: boolean;
|
|
16
16
|
onSave: (publish?: boolean) => Promise<void>;
|
|
17
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';
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export function EditorHeader({
|
|
@@ -27,6 +32,11 @@ export function EditorHeader({
|
|
|
27
32
|
isSaving,
|
|
28
33
|
onSave,
|
|
29
34
|
onSaveError,
|
|
35
|
+
autoSaveEnabled = false,
|
|
36
|
+
onAutoSaveToggle,
|
|
37
|
+
isDirty = false,
|
|
38
|
+
autoSaveCountdown = null,
|
|
39
|
+
autoSaveStatus = 'idle',
|
|
30
40
|
}: EditorHeaderProps) {
|
|
31
41
|
const { state, dispatch } = useEditor();
|
|
32
42
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
@@ -49,21 +59,21 @@ export function EditorHeader({
|
|
|
49
59
|
try {
|
|
50
60
|
const targetStatus = saveAsDraft ? 'draft' : 'published';
|
|
51
61
|
console.log('[EditorHeader] Starting save process...', { saveAsDraft, targetStatus, currentStatus: state.status });
|
|
52
|
-
|
|
62
|
+
|
|
53
63
|
// Set status before saving - ensure state is updated
|
|
54
64
|
if (saveAsDraft) {
|
|
55
65
|
dispatch({ type: 'SET_STATUS', payload: 'draft' });
|
|
56
66
|
} else {
|
|
57
67
|
dispatch({ type: 'SET_STATUS', payload: 'published' });
|
|
58
68
|
}
|
|
59
|
-
|
|
69
|
+
|
|
60
70
|
// Wait longer to ensure state update propagates through the reducer and context
|
|
61
71
|
// React state updates are asynchronous, so we need to wait for the state to actually update
|
|
62
72
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
63
|
-
|
|
73
|
+
|
|
64
74
|
// Verify status was updated
|
|
65
75
|
console.log('[EditorHeader] Status after update:', state.status, 'Expected:', targetStatus);
|
|
66
|
-
|
|
76
|
+
|
|
67
77
|
await onSave(!saveAsDraft);
|
|
68
78
|
console.log('[EditorHeader] Post saved successfully');
|
|
69
79
|
// Clear any previous errors
|
|
@@ -73,7 +83,7 @@ export function EditorHeader({
|
|
|
73
83
|
console.error('[EditorHeader] Failed to save post:', error);
|
|
74
84
|
// Extract user-friendly error message
|
|
75
85
|
let errorMessage = error.message || 'Failed to save post';
|
|
76
|
-
|
|
86
|
+
|
|
77
87
|
// Make error messages more user-friendly
|
|
78
88
|
if (errorMessage.includes('Missing required fields')) {
|
|
79
89
|
// Keep the detailed message about missing fields
|
|
@@ -85,7 +95,7 @@ export function EditorHeader({
|
|
|
85
95
|
} else if (errorMessage.includes('Failed to save')) {
|
|
86
96
|
errorMessage = 'Unable to save the post. Please check your connection and try again.';
|
|
87
97
|
}
|
|
88
|
-
|
|
98
|
+
|
|
89
99
|
setSaveError(errorMessage);
|
|
90
100
|
onSaveError(errorMessage);
|
|
91
101
|
// Re-throw the error so the modal knows it failed and doesn't show success
|
|
@@ -94,10 +104,20 @@ export function EditorHeader({
|
|
|
94
104
|
};
|
|
95
105
|
|
|
96
106
|
return (
|
|
97
|
-
<header className="flex items-center justify-between px-6 py-3 bg-dashboard-sidebar backdrop-blur-md
|
|
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">
|
|
98
108
|
<div className="flex items-center gap-6">
|
|
99
109
|
<button
|
|
100
|
-
onClick={() =>
|
|
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
|
+
}}
|
|
101
121
|
className="text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white transition-colors"
|
|
102
122
|
>
|
|
103
123
|
<ArrowLeft size={20} strokeWidth={1.5} />
|
|
@@ -114,15 +134,84 @@ export function EditorHeader({
|
|
|
114
134
|
</div>
|
|
115
135
|
|
|
116
136
|
<div className="flex items-center gap-4">
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
className=
|
|
120
|
-
|
|
121
|
-
|
|
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'
|
|
122
192
|
}`}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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>
|
|
126
215
|
{/* Save Draft Button - Always visible for drafts and new posts */}
|
|
127
216
|
{(state.status === 'draft' || !state.postId) && (
|
|
128
217
|
<button
|