@jhits/plugin-blog 0.0.19 → 0.0.20
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/dist/api/categories.d.ts.map +1 -1
- package/dist/api/categories.js +42 -38
- package/dist/api/handler.d.ts +1 -26
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +81 -490
- package/dist/api/router.d.ts +0 -5
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +8 -35
- package/dist/api/service.d.ts +80 -0
- package/dist/api/service.d.ts.map +1 -0
- package/dist/api/service.js +219 -0
- package/dist/hooks/useAutoSave.d.ts +10 -0
- package/dist/hooks/useAutoSave.d.ts.map +1 -0
- package/dist/hooks/useAutoSave.js +57 -0
- package/dist/hooks/useCategories.d.ts +1 -1
- package/dist/hooks/useCategories.d.ts.map +1 -1
- package/dist/hooks/useCategories.js +15 -46
- package/dist/index.d.ts +24 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -201
- package/dist/init.d.ts +20 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +8 -7
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
- package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
- package/dist/lib/layouts/index.d.ts +3 -3
- package/dist/lib/layouts/index.js +4 -4
- package/dist/lib/mappers/apiMapper.d.ts +10 -0
- package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
- package/dist/lib/mappers/apiMapper.js +47 -32
- package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
- package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
- package/dist/lib/rich-text/RichTextEditor.js +12 -9
- package/dist/lib/utils/config-resolver.d.ts +28 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +46 -0
- package/dist/lib/utils/tree.d.ts +29 -0
- package/dist/lib/utils/tree.d.ts.map +1 -0
- package/dist/lib/utils/tree.js +129 -0
- package/dist/state/EditorContext.d.ts +3 -25
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +124 -174
- package/dist/state/reducer.d.ts +1 -5
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/state/reducer.js +128 -521
- package/dist/state/types.d.ts +12 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/post.d.ts +17 -1
- package/dist/types/post.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
- package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +34 -482
- package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorHeader.js +27 -63
- package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
- package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
- package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
- package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
- package/dist/views/CanvasEditor/components/index.d.ts +2 -0
- package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/index.js +1 -0
- package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
- package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
- package/dist/views/PostManager/EmptyState.d.ts +1 -1
- package/dist/views/PostManager/EmptyState.js +4 -4
- package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
- package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
- package/dist/views/PostManager/FilterDropdown.js +28 -0
- package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
- package/dist/views/PostManager/LanguageFlags.js +4 -1
- package/dist/views/PostManager/PostCards.d.ts.map +1 -1
- package/dist/views/PostManager/PostCards.js +23 -40
- package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
- package/dist/views/PostManager/PostFilters.js +34 -3
- package/dist/views/PostManager/PostManagerView.d.ts +1 -2
- package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
- package/dist/views/PostManager/PostManagerView.js +30 -96
- package/dist/views/PostManager/PostStats.d.ts.map +1 -1
- package/dist/views/PostManager/PostStats.js +10 -10
- package/dist/views/PostManager/PostTable.d.ts.map +1 -1
- package/dist/views/PostManager/PostTable.js +23 -40
- package/dist/views/Settings/SettingsView.d.ts +1 -1
- package/dist/views/Settings/SettingsView.d.ts.map +1 -1
- package/dist/views/Settings/SettingsView.js +12 -39
- package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
- package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
- package/package.json +42 -6
- package/src/api/categories.ts +48 -52
- package/src/api/handler.ts +87 -594
- package/src/api/router.ts +15 -65
- package/src/api/service.ts +241 -0
- package/src/hooks/useAutoSave.ts +64 -0
- package/src/hooks/useCategories.ts +19 -47
- package/src/index.tsx +79 -293
- package/src/init.tsx +24 -11
- package/src/lib/blocks/BlockRenderer.tsx +1 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
- package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
- package/src/lib/layouts/index.ts +4 -4
- package/src/lib/mappers/apiMapper.ts +63 -32
- package/src/lib/rich-text/RichTextEditor.tsx +16 -9
- package/src/lib/utils/config-resolver.ts +64 -0
- package/src/lib/utils/tree.ts +150 -0
- package/src/state/EditorContext.tsx +153 -232
- package/src/state/reducer.ts +141 -606
- package/src/state/types.ts +14 -1
- package/src/types/block.ts +10 -0
- package/src/types/post.ts +19 -1
- package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
- package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
- package/src/views/CanvasEditor/EditorBody.tsx +98 -610
- package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
- package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
- package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
- package/src/views/CanvasEditor/components/index.ts +2 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
- package/src/views/PostManager/EmptyState.tsx +9 -10
- package/src/views/PostManager/FilterDropdown.tsx +95 -0
- package/src/views/PostManager/LanguageFlags.tsx +6 -2
- package/src/views/PostManager/PostCards.tsx +127 -133
- package/src/views/PostManager/PostFilters.tsx +73 -68
- package/src/views/PostManager/PostManagerView.tsx +132 -179
- package/src/views/PostManager/PostStats.tsx +21 -20
- package/src/views/PostManager/PostTable.tsx +137 -165
- package/src/views/Settings/SettingsView.tsx +64 -180
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
- package/src/hooks/index.d.ts +0 -8
- package/src/hooks/index.d.ts.map +0 -1
- package/src/hooks/useBlog.d.ts +0 -31
- package/src/hooks/useBlog.d.ts.map +0 -1
- package/src/hooks/useBlogs.d.ts +0 -39
- package/src/hooks/useBlogs.d.ts.map +0 -1
- package/src/hooks/useCategories.d.ts +0 -9
- package/src/hooks/useCategories.d.ts.map +0 -1
- package/src/lib/blocks/BlockRenderer.d.ts +0 -54
- package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
- package/src/lib/config-storage.d.ts +0 -30
- package/src/lib/config-storage.d.ts.map +0 -1
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
- package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
- package/src/lib/layouts/index.d.ts +0 -23
- package/src/lib/layouts/index.d.ts.map +0 -1
- package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
- package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
- package/src/lib/mappers/apiMapper.d.ts +0 -66
- package/src/lib/mappers/apiMapper.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
- package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
- package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
- package/src/lib/rich-text/index.d.ts +0 -9
- package/src/lib/rich-text/index.d.ts.map +0 -1
- package/src/lib/utils/blockHelpers.d.ts +0 -23
- package/src/lib/utils/blockHelpers.d.ts.map +0 -1
- package/src/lib/utils/configValidation.d.ts +0 -23
- package/src/lib/utils/configValidation.d.ts.map +0 -1
- package/src/registry/BlockRegistry.d.ts +0 -62
- package/src/registry/BlockRegistry.d.ts.map +0 -1
- package/src/registry/index.d.ts +0 -6
- package/src/registry/index.d.ts.map +0 -1
- package/src/state/EditorContext.d.ts +0 -45
- package/src/state/EditorContext.d.ts.map +0 -1
- package/src/state/index.d.ts +0 -7
- package/src/state/index.d.ts.map +0 -1
- package/src/state/reducer.d.ts +0 -11
- package/src/state/reducer.d.ts.map +0 -1
- package/src/state/types.d.ts +0 -162
- package/src/state/types.d.ts.map +0 -1
- package/src/types/block.d.ts +0 -221
- package/src/types/block.d.ts.map +0 -1
- package/src/types/index.d.ts +0 -8
- package/src/types/index.d.ts.map +0 -1
- package/src/types/post.d.ts +0 -136
- package/src/types/post.d.ts.map +0 -1
- package/src/utils/client.d.ts +0 -48
- package/src/utils/client.d.ts.map +0 -1
- package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
- package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
- package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
- package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
- package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
- package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
- package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
- package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/index.d.ts +0 -21
- package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
- package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
- package/src/views/CanvasEditor/index.d.ts +0 -16
- package/src/views/CanvasEditor/index.d.ts.map +0 -1
- package/src/views/PostManager/EmptyState.d.ts +0 -10
- package/src/views/PostManager/EmptyState.d.ts.map +0 -1
- package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
- package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
- package/src/views/PostManager/PostCards.d.ts +0 -15
- package/src/views/PostManager/PostCards.d.ts.map +0 -1
- package/src/views/PostManager/PostFilters.d.ts +0 -16
- package/src/views/PostManager/PostFilters.d.ts.map +0 -1
- package/src/views/PostManager/PostManagerView.d.ts +0 -11
- package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
- package/src/views/PostManager/PostStats.d.ts +0 -11
- package/src/views/PostManager/PostStats.d.ts.map +0 -1
- package/src/views/PostManager/PostTable.d.ts +0 -15
- package/src/views/PostManager/PostTable.d.ts.map +0 -1
- package/src/views/PostManager/index.d.ts +0 -12
- package/src/views/PostManager/index.d.ts.map +0 -1
- package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
- package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
- package/src/views/Preview/index.d.ts +0 -6
- package/src/views/Preview/index.d.ts.map +0 -1
- package/src/views/Settings/SettingsView.d.ts +0 -10
- package/src/views/Settings/SettingsView.d.ts.map +0 -1
- package/src/views/Settings/index.d.ts +0 -6
- package/src/views/Settings/index.d.ts.map +0 -1
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
- package/src/views/SlugSEO/index.d.ts +0 -6
- package/src/views/SlugSEO/index.d.ts.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import { Shield, Key, Users } from 'lucide-react';
|
|
4
|
+
import { Shield, Key, Users, Lock, Unlock, EyeOff } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
export interface PrivacySettings {
|
|
7
7
|
isPrivate?: boolean;
|
|
@@ -14,35 +14,25 @@ export interface PrivacySettingsSectionProps {
|
|
|
14
14
|
onUpdate: (privacy: PrivacySettings) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Privacy Settings Section Component
|
|
19
|
-
* Handles privacy settings: private, password-protected, share with users
|
|
20
|
-
*/
|
|
21
17
|
export function PrivacySettingsSection({
|
|
22
18
|
privacy,
|
|
23
19
|
onUpdate,
|
|
24
20
|
}: PrivacySettingsSectionProps) {
|
|
25
|
-
const [users, setUsers] = useState<Array<{ _id: string; name: string; email: string }>>([]);
|
|
21
|
+
const [users, setUsers] = useState<Array<{ _id: string; name: string; email: string; image?: string }>>([]);
|
|
26
22
|
const [loadingUsers, setLoadingUsers] = useState(false);
|
|
27
23
|
const [showPasswordInput, setShowPasswordInput] = useState(false);
|
|
28
24
|
const [passwordValue, setPasswordValue] = useState(privacy?.password || '');
|
|
29
25
|
const [showUserSelector, setShowUserSelector] = useState(false);
|
|
30
26
|
|
|
31
|
-
// Fetch users from plugin-users
|
|
32
27
|
useEffect(() => {
|
|
33
28
|
const fetchUsers = async () => {
|
|
34
29
|
try {
|
|
35
30
|
setLoadingUsers(true);
|
|
36
31
|
const res = await fetch('/api/users');
|
|
37
32
|
const data = await res.json();
|
|
38
|
-
if (Array.isArray(data))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch (err) {
|
|
42
|
-
console.error('Failed to load users', err);
|
|
43
|
-
} finally {
|
|
44
|
-
setLoadingUsers(false);
|
|
45
|
-
}
|
|
33
|
+
if (Array.isArray(data)) setUsers(data);
|
|
34
|
+
} catch (err) { console.error(err); }
|
|
35
|
+
finally { setLoadingUsers(false); }
|
|
46
36
|
};
|
|
47
37
|
fetchUsers();
|
|
48
38
|
}, []);
|
|
@@ -51,17 +41,13 @@ export function PrivacySettingsSection({
|
|
|
51
41
|
onUpdate({
|
|
52
42
|
...privacy,
|
|
53
43
|
isPrivate,
|
|
54
|
-
// Clear password and shared users if making public
|
|
55
44
|
...(isPrivate ? {} : { password: undefined, sharedWithUsers: undefined }),
|
|
56
45
|
});
|
|
57
46
|
};
|
|
58
47
|
|
|
59
48
|
const handlePasswordChange = (password: string) => {
|
|
60
49
|
setPasswordValue(password);
|
|
61
|
-
onUpdate({
|
|
62
|
-
...privacy,
|
|
63
|
-
password: password || undefined,
|
|
64
|
-
});
|
|
50
|
+
onUpdate({ ...privacy, password: password || undefined });
|
|
65
51
|
};
|
|
66
52
|
|
|
67
53
|
const handleUserToggle = (userId: string) => {
|
|
@@ -69,144 +55,107 @@ export function PrivacySettingsSection({
|
|
|
69
55
|
const newUsers = currentUsers.includes(userId)
|
|
70
56
|
? currentUsers.filter(id => id !== userId)
|
|
71
57
|
: [...currentUsers, userId];
|
|
72
|
-
onUpdate({
|
|
73
|
-
...privacy,
|
|
74
|
-
sharedWithUsers: newUsers.length > 0 ? newUsers : undefined,
|
|
75
|
-
});
|
|
58
|
+
onUpdate({ ...privacy, sharedWithUsers: newUsers.length > 0 ? newUsers : undefined });
|
|
76
59
|
};
|
|
77
60
|
|
|
78
61
|
return (
|
|
79
|
-
<section className="
|
|
80
|
-
<div className="flex items-center gap-3
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
62
|
+
<section className="space-y-6">
|
|
63
|
+
<div className="flex items-center gap-3">
|
|
64
|
+
<div className="size-8 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
|
|
65
|
+
<Shield size={14} />
|
|
66
|
+
</div>
|
|
67
|
+
<label className="text-[10px] font-black text-dashboard-text uppercase tracking-[0.3em]">
|
|
68
|
+
Privacy & Security
|
|
84
69
|
</label>
|
|
85
70
|
</div>
|
|
71
|
+
|
|
86
72
|
<div className="space-y-4">
|
|
87
|
-
{/*
|
|
88
|
-
<div className=
|
|
89
|
-
<div>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
{/* 1. Visibility Toggle */}
|
|
74
|
+
<div className={`p-4 rounded-[1.5rem] border transition-all duration-500 ${privacy?.isPrivate ? 'bg-primary/5 border-primary/20 shadow-sm' : 'bg-dashboard-bg/50 border-dashboard-border/50'}`}>
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div className="flex items-center gap-3">
|
|
77
|
+
<div className={`size-10 rounded-xl flex items-center justify-center border transition-all ${privacy?.isPrivate ? 'bg-primary text-white border-primary/20' : 'bg-dashboard-card text-dashboard-text-secondary border-dashboard-border'}`}>
|
|
78
|
+
{privacy?.isPrivate ? <Lock size={16} /> : <Unlock size={16} />}
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<label className="text-[10px] font-black text-dashboard-text uppercase tracking-widest">Restrict Access</label>
|
|
82
|
+
<p className="text-[9px] text-dashboard-text-secondary italic opacity-60">Hide from public view</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => handlePrivacyToggle(!privacy?.isPrivate)}
|
|
87
|
+
className={`relative w-12 h-6 rounded-full transition-colors ${privacy?.isPrivate ? 'bg-primary' : 'bg-dashboard-card border border-dashboard-border'}`}
|
|
88
|
+
>
|
|
89
|
+
<div className={`absolute top-1 left-1 size-4 rounded-full transition-transform ${privacy?.isPrivate ? 'translate-x-6 bg-white shadow-sm' : 'translate-x-0 bg-dashboard-text-secondary/30'}`} />
|
|
90
|
+
</button>
|
|
96
91
|
</div>
|
|
97
|
-
<button
|
|
98
|
-
onClick={() => handlePrivacyToggle(!privacy?.isPrivate)}
|
|
99
|
-
className={`relative w-12 h-6 rounded-full transition-colors ${privacy?.isPrivate ? 'bg-primary' : 'bg-neutral-300 dark:bg-neutral-700'
|
|
100
|
-
}`}
|
|
101
|
-
>
|
|
102
|
-
<div className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform ${privacy?.isPrivate ? 'translate-x-6' : 'translate-x-0'
|
|
103
|
-
}`} />
|
|
104
|
-
</button>
|
|
105
92
|
</div>
|
|
106
93
|
|
|
107
|
-
{/* Password
|
|
94
|
+
{/* 2. Password & Users (Only if Private) */}
|
|
108
95
|
{privacy?.isPrivate && (
|
|
109
|
-
<div className="space-y-2">
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
96
|
+
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
97
|
+
{/* Password */}
|
|
98
|
+
<div className="group">
|
|
99
|
+
<div className="flex items-center justify-between mb-2 px-1">
|
|
100
|
+
<label className="text-[9px] font-black text-dashboard-text-secondary uppercase tracking-widest group-focus-within:text-primary transition-colors flex items-center gap-2">
|
|
101
|
+
<Key size={10} /> Password Layer
|
|
114
102
|
</label>
|
|
115
|
-
<p className="text-[9px] text-neutral-500 dark:text-neutral-400">
|
|
116
|
-
Require password to view
|
|
117
|
-
</p>
|
|
118
103
|
</div>
|
|
119
|
-
<button
|
|
120
|
-
onClick={() => {
|
|
121
|
-
setShowPasswordInput(!showPasswordInput);
|
|
122
|
-
if (!showPasswordInput && !privacy.password) {
|
|
123
|
-
setPasswordValue('');
|
|
124
|
-
}
|
|
125
|
-
}}
|
|
126
|
-
className={`p-1.5 rounded-lg transition-colors ${privacy.password || showPasswordInput
|
|
127
|
-
? 'bg-primary/10 text-primary'
|
|
128
|
-
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400'
|
|
129
|
-
}`}
|
|
130
|
-
>
|
|
131
|
-
<Key size={14} />
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
{(showPasswordInput || privacy.password) && (
|
|
135
104
|
<input
|
|
136
105
|
type="password"
|
|
137
106
|
value={passwordValue}
|
|
138
107
|
onChange={(e) => handlePasswordChange(e.target.value)}
|
|
139
|
-
placeholder="
|
|
140
|
-
className="w-full px-
|
|
108
|
+
placeholder="Set viewing password..."
|
|
109
|
+
className="w-full px-4 py-2.5 bg-dashboard-bg/50 border border-dashboard-border/50 rounded-xl text-xs font-bold text-dashboard-text placeholder:text-dashboard-text-secondary/30 outline-none focus:border-primary/40 focus:bg-primary/[0.02] transition-all"
|
|
141
110
|
/>
|
|
142
|
-
|
|
143
|
-
</div>
|
|
144
|
-
)}
|
|
111
|
+
</div>
|
|
145
112
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<div className="space-y-2">
|
|
149
|
-
<div className="flex items-center justify-between">
|
|
150
|
-
<div>
|
|
151
|
-
<label className="text-[10px] text-neutral-700 dark:text-neutral-300 font-bold block mb-1">
|
|
152
|
-
Share with Users
|
|
153
|
-
</label>
|
|
154
|
-
<p className="text-[9px] text-neutral-500 dark:text-neutral-400">
|
|
155
|
-
Grant access to specific users
|
|
156
|
-
</p>
|
|
157
|
-
</div>
|
|
113
|
+
{/* User Selection */}
|
|
114
|
+
<div className="pt-2">
|
|
158
115
|
<button
|
|
159
116
|
onClick={() => setShowUserSelector(!showUserSelector)}
|
|
160
|
-
className={`p-
|
|
161
|
-
? 'bg-primary/10 text-primary'
|
|
162
|
-
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400'
|
|
163
|
-
}`}
|
|
117
|
+
className={`w-full flex items-center justify-between p-3 rounded-xl border transition-all ${showUserSelector ? 'bg-primary/10 border-primary/30 text-primary' : 'bg-dashboard-card/50 border-dashboard-border/50 text-dashboard-text-secondary hover:border-primary/30'}`}
|
|
164
118
|
>
|
|
165
|
-
<
|
|
119
|
+
<div className="flex items-center gap-2 text-[9px] font-black uppercase tracking-widest">
|
|
120
|
+
<Users size={12} />
|
|
121
|
+
<span>Grant Personal Access</span>
|
|
122
|
+
</div>
|
|
123
|
+
<span className="text-[10px] font-bold opacity-60 bg-white/5 px-2 py-0.5 rounded-full">
|
|
124
|
+
{privacy.sharedWithUsers?.length || 0}
|
|
125
|
+
</span>
|
|
166
126
|
</button>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<div className="space-y-2">
|
|
176
|
-
{users.map((user) => (
|
|
177
|
-
<label
|
|
178
|
-
key={user._id}
|
|
179
|
-
className="flex items-center gap-2 cursor-pointer hover:bg-dashboard-bg p-2 rounded transition-colors"
|
|
180
|
-
>
|
|
127
|
+
|
|
128
|
+
{showUserSelector && (
|
|
129
|
+
<div className="mt-3 bg-dashboard-bg/80 backdrop-blur-xl rounded-2xl border border-dashboard-border/50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-300">
|
|
130
|
+
<div className="max-h-48 overflow-y-auto p-2 custom-scrollbar space-y-1">
|
|
131
|
+
{loadingUsers ? (
|
|
132
|
+
<div className="p-4 text-center animate-pulse text-[10px] font-black text-primary/40 uppercase tracking-widest">Scanning Network...</div>
|
|
133
|
+
) : users.map((user) => (
|
|
134
|
+
<label key={user._id} className="flex items-center gap-3 p-2 rounded-xl hover:bg-primary/5 cursor-pointer transition-all group/user">
|
|
181
135
|
<input
|
|
182
136
|
type="checkbox"
|
|
183
137
|
checked={privacy.sharedWithUsers?.includes(user._id) || false}
|
|
184
138
|
onChange={() => handleUserToggle(user._id)}
|
|
185
|
-
className="rounded border-
|
|
139
|
+
className="size-4 rounded border-dashboard-border text-primary focus:ring-primary/20 bg-dashboard-card"
|
|
186
140
|
/>
|
|
187
|
-
<div className="flex-
|
|
188
|
-
<
|
|
189
|
-
{user.name}
|
|
190
|
-
</
|
|
191
|
-
<
|
|
192
|
-
{user.
|
|
193
|
-
|
|
141
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
142
|
+
<div className="size-7 rounded-lg bg-dashboard-card border border-dashboard-border flex items-center justify-center text-[10px] font-black overflow-hidden shrink-0">
|
|
143
|
+
{user.image ? <img src={user.image} className="size-full object-cover" crossOrigin="anonymous" /> : user.name[0]}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="min-w-0">
|
|
146
|
+
<p className="text-[10px] font-black text-dashboard-text uppercase truncate">{user.name}</p>
|
|
147
|
+
<p className="text-[8px] text-dashboard-text-secondary truncate opacity-60 italic">{user.email}</p>
|
|
148
|
+
</div>
|
|
194
149
|
</div>
|
|
195
150
|
</label>
|
|
196
151
|
))}
|
|
197
152
|
</div>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
{privacy.sharedWithUsers && privacy.sharedWithUsers.length > 0 && (
|
|
202
|
-
<p className="text-[9px] text-neutral-500 dark:text-neutral-400">
|
|
203
|
-
Shared with {privacy.sharedWithUsers.length} user{privacy.sharedWithUsers.length !== 1 ? 's' : ''}
|
|
204
|
-
</p>
|
|
205
|
-
)}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
206
156
|
</div>
|
|
207
157
|
)}
|
|
208
158
|
</div>
|
|
209
159
|
</section>
|
|
210
160
|
);
|
|
211
161
|
}
|
|
212
|
-
|
|
@@ -25,4 +25,5 @@ export { EditorCanvas } from './EditorCanvas';
|
|
|
25
25
|
export type { EditorCanvasProps } from './EditorCanvas';
|
|
26
26
|
|
|
27
27
|
export { EditorSidebar } from './EditorSidebar';
|
|
28
|
-
export type { EditorSidebarProps } from './EditorSidebar';
|
|
28
|
+
export type { EditorSidebarProps } from './EditorSidebar';export { JSONInspector } from './JSONInspector';
|
|
29
|
+
export type { JSONInspectorProps } from './JSONInspector';
|
|
@@ -36,12 +36,13 @@ export function useHeroBlock(state: EditorState, registeredBlocks: any[]) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// If no hero block in contentBlocks, initialize from defaults
|
|
39
|
-
//
|
|
39
|
+
// We map metadata.excerpt to both 'summary' and 'description' to support different block implementations
|
|
40
40
|
const initialData = {
|
|
41
41
|
...heroData,
|
|
42
42
|
title: state.title || heroData.title || '',
|
|
43
43
|
summary: state.metadata.excerpt || heroData.summary || '',
|
|
44
|
-
|
|
44
|
+
description: state.metadata.excerpt || heroData.description || '',
|
|
45
|
+
category: state.metadata.categories?.[0] || heroData.category || '',
|
|
45
46
|
};
|
|
46
47
|
return {
|
|
47
48
|
id: generateBlockId(),
|
|
@@ -55,40 +56,36 @@ export function useHeroBlock(state: EditorState, registeredBlocks: any[]) {
|
|
|
55
56
|
} else {
|
|
56
57
|
setHeroBlock(null);
|
|
57
58
|
}
|
|
58
|
-
}, [heroBlockDefinition, state.blocks, state.title, state.metadata.excerpt]);
|
|
59
|
+
}, [heroBlockDefinition, state.blocks, state.title, state.metadata.excerpt, state.metadata.categories]);
|
|
59
60
|
|
|
60
61
|
// Sync hero block with editor state when post is loaded (for existing posts)
|
|
61
|
-
// BUT: Never sync image from featured image to hero - they are independent
|
|
62
|
-
// Only sync title, summary, and category
|
|
63
62
|
useEffect(() => {
|
|
64
63
|
if (heroBlock && heroBlockDefinition && state.postId) {
|
|
65
|
-
// Only update if the hero block data doesn't match the editor state
|
|
66
|
-
// This prevents overwriting user edits with stale data
|
|
67
64
|
const currentTitle = (heroBlock.data as any)?.title || '';
|
|
68
65
|
const currentSummary = (heroBlock.data as any)?.summary || '';
|
|
66
|
+
const currentDescription = (heroBlock.data as any)?.description || '';
|
|
69
67
|
const currentCategory = (heroBlock.data as any)?.category || '';
|
|
70
68
|
|
|
71
69
|
const stateTitle = state.title || '';
|
|
72
|
-
const
|
|
70
|
+
const stateExcerpt = state.metadata.excerpt || '';
|
|
73
71
|
const stateCategory = state.metadata.categories?.[0] || '';
|
|
74
72
|
|
|
75
|
-
// Check if hero block is out of sync with editor state
|
|
76
|
-
// NOTE: We do NOT sync image anymore - hero and featured image are independent
|
|
77
73
|
const titleMismatch = currentTitle !== stateTitle;
|
|
78
|
-
|
|
74
|
+
// Check both summary and description for mismatches
|
|
75
|
+
const summaryMismatch = currentSummary !== stateExcerpt;
|
|
76
|
+
const descriptionMismatch = currentDescription !== stateExcerpt;
|
|
79
77
|
const categoryMismatch = currentCategory !== stateCategory;
|
|
80
78
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
if ((titleMismatch || summaryMismatch || categoryMismatch) && (stateTitle || stateSummary || stateCategory)) {
|
|
79
|
+
// Sync if state has values and there's a mismatch
|
|
80
|
+
if ((titleMismatch || summaryMismatch || descriptionMismatch || categoryMismatch) && (stateTitle || stateExcerpt || stateCategory)) {
|
|
84
81
|
setHeroBlock({
|
|
85
82
|
...heroBlock,
|
|
86
83
|
data: {
|
|
87
84
|
...heroBlock.data,
|
|
88
|
-
title: stateTitle ||
|
|
89
|
-
summary:
|
|
90
|
-
|
|
91
|
-
category: stateCategory ||
|
|
85
|
+
title: stateTitle || currentTitle,
|
|
86
|
+
summary: stateExcerpt || currentSummary,
|
|
87
|
+
description: stateExcerpt || currentDescription,
|
|
88
|
+
category: stateCategory || currentCategory,
|
|
92
89
|
},
|
|
93
90
|
});
|
|
94
91
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import { useEffect, useState, useRef } from 'react';
|
|
2
4
|
import { apiToBlogPost, type APIBlogDocument } from '../../../lib/mappers/apiMapper';
|
|
3
5
|
import type { BlogPost } from '../../../types/post';
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Technical hook for loading post data into the Orchestrator
|
|
9
|
+
*/
|
|
5
10
|
export function usePostLoader(
|
|
6
11
|
postId: string | undefined,
|
|
7
12
|
currentPostId: string | null,
|
|
@@ -10,41 +15,44 @@ export function usePostLoader(
|
|
|
10
15
|
language?: string
|
|
11
16
|
) {
|
|
12
17
|
const [isLoadingPost, setIsLoadingPost] = useState(false);
|
|
13
|
-
// Use a ref to track language so changes don't re-trigger initial load
|
|
14
18
|
const languageRef = useRef(language);
|
|
15
19
|
languageRef.current = language;
|
|
16
20
|
|
|
17
21
|
useEffect(() => {
|
|
22
|
+
// Only trigger if we have a target ID but no active post in state
|
|
23
|
+
// This prevents infinite loops while allowing initial synchronization
|
|
18
24
|
if (postId && !currentPostId) {
|
|
19
25
|
const loadPostData = async () => {
|
|
20
26
|
try {
|
|
21
27
|
setIsLoadingPost(true);
|
|
22
|
-
// Reset hero block before loading new post so it gets re-initialized from the new post's blocks
|
|
23
28
|
resetHeroBlock();
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
const lang = languageRef.current || 'nl';
|
|
31
|
+
// We use the relative URL which will hit the Dashboard API
|
|
32
|
+
const url = `/api/plugin-blog/${postId}?language=${lang}&admin=true`;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
const response = await fetch(url);
|
|
29
37
|
if (!response.ok) {
|
|
30
|
-
throw new Error(
|
|
38
|
+
throw new Error(`NODE_HANDSHAKE_FAILED: ${response.status}`);
|
|
31
39
|
}
|
|
40
|
+
|
|
32
41
|
const apiDoc: APIBlogDocument = await response.json();
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
const blogPost = apiToBlogPost(apiDoc);
|
|
34
45
|
loadPost(blogPost);
|
|
35
46
|
} catch (error) {
|
|
36
|
-
console.error('
|
|
37
|
-
alert('Failed to load post. Please try again.');
|
|
47
|
+
console.error('[usePostLoader] CRITICAL_SYNC_ERROR:', error);
|
|
38
48
|
} finally {
|
|
39
49
|
setIsLoadingPost(false);
|
|
40
50
|
}
|
|
41
51
|
};
|
|
52
|
+
|
|
42
53
|
loadPostData();
|
|
43
54
|
}
|
|
44
|
-
|
|
45
|
-
// Language switching is handled separately by handleLanguageChange
|
|
46
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
47
|
-
}, [postId, currentPostId]);
|
|
55
|
+
}, [postId, currentPostId, loadPost, resetHeroBlock]);
|
|
48
56
|
|
|
49
57
|
return { isLoadingPost };
|
|
50
58
|
}
|
|
@@ -89,7 +89,7 @@ export function useUnsavedChanges({
|
|
|
89
89
|
try {
|
|
90
90
|
localStorage.setItem(AUTO_SAVE_STORAGE_KEY, enabled.toString());
|
|
91
91
|
setAutoSaveEnabledState(enabled);
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
} catch (error) {
|
|
94
94
|
console.error('[useUnsavedChanges] Failed to save auto-save preference:', error);
|
|
95
95
|
}
|
|
@@ -117,7 +117,7 @@ export function useUnsavedChanges({
|
|
|
117
117
|
// When a post is loaded (postId exists) and isDirty is false, update the saved state reference
|
|
118
118
|
if (postId && !isDirty && lastSavedStateRef.current === '') {
|
|
119
119
|
lastSavedStateRef.current = getStateSnapshot();
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
}
|
|
122
122
|
// Also update if isDirty becomes false after being true (e.g., after save or MARK_CLEAN)
|
|
123
123
|
if (!isDirty && lastSavedStateRef.current !== getStateSnapshot()) {
|
|
@@ -136,11 +136,11 @@ export function useUnsavedChanges({
|
|
|
136
136
|
setSaveStatus('saving');
|
|
137
137
|
setCountdown(null);
|
|
138
138
|
countdownStartTimeRef.current = null;
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
await onSave(heroBlock);
|
|
141
141
|
lastSavedStateRef.current = getStateSnapshot();
|
|
142
142
|
setSaveStatus('saved');
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
|
|
145
145
|
// Clear save status after 2 seconds
|
|
146
146
|
if (saveStatusTimeoutRef.current) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Empty State Component
|
|
3
|
-
*
|
|
3
|
+
* Generic empty state for when no posts are found
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import { PenTool, Plus } from 'lucide-react';
|
|
10
10
|
|
|
11
11
|
export interface EmptyStateProps {
|
|
12
12
|
hasFilters: boolean;
|
|
@@ -16,27 +16,26 @@ export interface EmptyStateProps {
|
|
|
16
16
|
export function EmptyState({ hasFilters, onCreatePost }: EmptyStateProps) {
|
|
17
17
|
return (
|
|
18
18
|
<div className="flex flex-col items-center justify-center py-20 px-8 bg-neutral-100 dark:bg-neutral-800/50 rounded-[2.5rem] border-2 border-dashed border-neutral-300 dark:border-neutral-700">
|
|
19
|
-
<div className="w-24 h-24 rounded-full bg-
|
|
20
|
-
<
|
|
19
|
+
<div className="w-24 h-24 rounded-full bg-primary/10 dark:bg-primary/20 flex items-center justify-center mb-6">
|
|
20
|
+
<PenTool className="text-primary size-12" />
|
|
21
21
|
</div>
|
|
22
22
|
<h3 className="text-xl font-black text-neutral-950 dark:text-white uppercase tracking-tight mb-2">
|
|
23
|
-
{hasFilters ? 'No
|
|
23
|
+
{hasFilters ? 'No Results Found' : 'Your Journal is Empty'}
|
|
24
24
|
</h3>
|
|
25
|
-
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center mb-6 max-w-md">
|
|
25
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center mb-6 max-w-md leading-relaxed">
|
|
26
26
|
{hasFilters
|
|
27
27
|
? 'Try adjusting your search or filter criteria to find what you\'re looking for.'
|
|
28
|
-
: '
|
|
28
|
+
: 'It looks like you haven\'t published any stories yet. Start writing to share your insights with the world.'}
|
|
29
29
|
</p>
|
|
30
30
|
{!hasFilters && (
|
|
31
31
|
<button
|
|
32
32
|
onClick={onCreatePost}
|
|
33
|
-
className="inline-flex items-center gap-2 px-
|
|
33
|
+
className="inline-flex items-center gap-2 px-8 py-4 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20"
|
|
34
34
|
>
|
|
35
35
|
<Plus size={16} />
|
|
36
|
-
|
|
36
|
+
Start Writing
|
|
37
37
|
</button>
|
|
38
38
|
)}
|
|
39
39
|
</div>
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
-
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Dropdown Component
|
|
3
|
+
* Custom styled dropdown matching the dashboard design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
9
|
+
import { ChevronDown } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
interface Option {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
icon?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FilterDropdownProps {
|
|
18
|
+
options: Option[];
|
|
19
|
+
value: string;
|
|
20
|
+
onChange: (value: string) => void;
|
|
21
|
+
icon: React.ReactNode;
|
|
22
|
+
label: string;
|
|
23
|
+
minWidth?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function FilterDropdown({
|
|
27
|
+
options,
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
icon,
|
|
31
|
+
label,
|
|
32
|
+
minWidth = '160px'
|
|
33
|
+
}: FilterDropdownProps) {
|
|
34
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
const selectedOption = options.find(opt => opt.value === value) || options[0];
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
41
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
42
|
+
setIsOpen(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
46
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="relative" ref={dropdownRef}>
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
53
|
+
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:bg-white/5 group ${
|
|
54
|
+
isOpen ? 'text-primary' : 'text-dashboard-text-secondary'
|
|
55
|
+
}`}
|
|
56
|
+
style={{ minWidth }}
|
|
57
|
+
>
|
|
58
|
+
<div className={`${isOpen ? 'text-primary' : 'text-dashboard-text-secondary group-hover:text-primary'} transition-colors`}>
|
|
59
|
+
{selectedOption.icon || icon}
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex flex-col items-start flex-1 overflow-hidden text-left">
|
|
62
|
+
<span className="text-[8px] font-black uppercase tracking-[0.2em] opacity-40">{label}</span>
|
|
63
|
+
<span className="text-[10px] font-black uppercase tracking-widest truncate w-full">
|
|
64
|
+
{selectedOption.label}
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
<ChevronDown className={`size-3 transition-transform duration-300 ${isOpen ? 'rotate-180 text-primary' : 'opacity-30'}`} />
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
{isOpen && (
|
|
71
|
+
<div className="absolute top-full left-0 mt-3 p-2 bg-dashboard-bg/95 backdrop-blur-2xl border border-dashboard-border rounded-2xl shadow-2xl z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-200">
|
|
72
|
+
{options.map((option) => (
|
|
73
|
+
<button
|
|
74
|
+
key={option.value}
|
|
75
|
+
onClick={() => {
|
|
76
|
+
onChange(option.value);
|
|
77
|
+
setIsOpen(false);
|
|
78
|
+
}}
|
|
79
|
+
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
|
|
80
|
+
option.value === value
|
|
81
|
+
? 'bg-primary/10 text-primary font-bold'
|
|
82
|
+
: 'text-dashboard-text-secondary hover:bg-white/5 hover:text-dashboard-text'
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<div className={option.value === value ? 'text-primary' : 'opacity-50'}>
|
|
86
|
+
{option.icon || icon}
|
|
87
|
+
</div>
|
|
88
|
+
<span className="text-[10px] font-black uppercase tracking-widest">{option.label}</span>
|
|
89
|
+
</button>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -60,7 +60,8 @@ export function LanguageFlags({ post }: LanguageFlagsProps) {
|
|
|
60
60
|
{post.availableLanguages.map((lang) => {
|
|
61
61
|
const langData = post.languages?.[lang];
|
|
62
62
|
const status = langData?.status || 'draft';
|
|
63
|
-
|
|
63
|
+
// Try to get title from metadata, fallback to root title
|
|
64
|
+
const langTitle = (langData as any)?.metadata?.title || post.title;
|
|
64
65
|
const isHovered = hoveredLang === lang;
|
|
65
66
|
|
|
66
67
|
return (
|
|
@@ -68,7 +69,10 @@ export function LanguageFlags({ post }: LanguageFlagsProps) {
|
|
|
68
69
|
key={lang}
|
|
69
70
|
ref={(el) => { flagRefs.current[lang] = el; }}
|
|
70
71
|
className="relative"
|
|
71
|
-
onMouseEnter={() =>
|
|
72
|
+
onMouseEnter={() => {
|
|
73
|
+
|
|
74
|
+
setHoveredLang(lang);
|
|
75
|
+
}}
|
|
72
76
|
onMouseLeave={() => setHoveredLang(null)}
|
|
73
77
|
>
|
|
74
78
|
<motion.div
|