@jhits/plugin-blog 0.0.18 → 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.
Files changed (291) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +42 -38
  3. package/dist/api/handler.d.ts +1 -26
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +81 -500
  6. package/dist/api/router.d.ts +0 -5
  7. package/dist/api/router.d.ts.map +1 -1
  8. package/dist/api/router.js +8 -35
  9. package/dist/api/service.d.ts +80 -0
  10. package/dist/api/service.d.ts.map +1 -0
  11. package/dist/api/service.js +219 -0
  12. package/dist/hooks/useAutoSave.d.ts +10 -0
  13. package/dist/hooks/useAutoSave.d.ts.map +1 -0
  14. package/dist/hooks/useAutoSave.js +57 -0
  15. package/dist/hooks/useCategories.d.ts +1 -1
  16. package/dist/hooks/useCategories.d.ts.map +1 -1
  17. package/dist/hooks/useCategories.js +15 -46
  18. package/dist/index.d.ts +24 -31
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +44 -201
  21. package/dist/init.d.ts +20 -7
  22. package/dist/init.d.ts.map +1 -1
  23. package/dist/init.js +8 -7
  24. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  25. package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
  26. package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
  27. package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
  28. package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
  29. package/dist/lib/layouts/index.d.ts +3 -3
  30. package/dist/lib/layouts/index.js +4 -4
  31. package/dist/lib/mappers/apiMapper.d.ts +10 -0
  32. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  33. package/dist/lib/mappers/apiMapper.js +47 -32
  34. package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
  35. package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
  36. package/dist/lib/rich-text/RichTextEditor.js +12 -9
  37. package/dist/lib/utils/config-resolver.d.ts +28 -0
  38. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  39. package/dist/lib/utils/config-resolver.js +46 -0
  40. package/dist/lib/utils/tree.d.ts +29 -0
  41. package/dist/lib/utils/tree.d.ts.map +1 -0
  42. package/dist/lib/utils/tree.js +129 -0
  43. package/dist/state/EditorContext.d.ts +3 -25
  44. package/dist/state/EditorContext.d.ts.map +1 -1
  45. package/dist/state/EditorContext.js +124 -174
  46. package/dist/state/reducer.d.ts +1 -5
  47. package/dist/state/reducer.d.ts.map +1 -1
  48. package/dist/state/reducer.js +128 -521
  49. package/dist/state/types.d.ts +12 -1
  50. package/dist/state/types.d.ts.map +1 -1
  51. package/dist/types/block.d.ts +9 -0
  52. package/dist/types/block.d.ts.map +1 -1
  53. package/dist/types/post.d.ts +17 -1
  54. package/dist/types/post.d.ts.map +1 -1
  55. package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
  56. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  57. package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
  58. package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
  59. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  60. package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
  61. package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
  62. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  63. package/dist/views/CanvasEditor/EditorBody.js +34 -482
  64. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  65. package/dist/views/CanvasEditor/EditorHeader.js +27 -63
  66. package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
  67. package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
  68. package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
  69. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
  70. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  71. package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
  72. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
  73. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
  74. package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
  75. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  76. package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
  77. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
  78. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
  79. package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
  80. package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
  81. package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
  82. package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
  83. package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
  84. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
  85. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
  86. package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
  87. package/dist/views/CanvasEditor/components/index.d.ts +2 -0
  88. package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
  89. package/dist/views/CanvasEditor/components/index.js +1 -0
  90. package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
  91. package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
  92. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
  93. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  94. package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
  95. package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
  96. package/dist/views/PostManager/EmptyState.d.ts +1 -1
  97. package/dist/views/PostManager/EmptyState.js +4 -4
  98. package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
  99. package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
  100. package/dist/views/PostManager/FilterDropdown.js +28 -0
  101. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
  102. package/dist/views/PostManager/LanguageFlags.js +4 -1
  103. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  104. package/dist/views/PostManager/PostCards.js +23 -40
  105. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  106. package/dist/views/PostManager/PostFilters.js +34 -3
  107. package/dist/views/PostManager/PostManagerView.d.ts +1 -2
  108. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  109. package/dist/views/PostManager/PostManagerView.js +30 -96
  110. package/dist/views/PostManager/PostStats.d.ts.map +1 -1
  111. package/dist/views/PostManager/PostStats.js +10 -10
  112. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  113. package/dist/views/PostManager/PostTable.js +23 -40
  114. package/dist/views/Settings/SettingsView.d.ts +1 -1
  115. package/dist/views/Settings/SettingsView.d.ts.map +1 -1
  116. package/dist/views/Settings/SettingsView.js +12 -39
  117. package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
  118. package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
  119. package/package.json +42 -6
  120. package/src/api/categories.ts +48 -52
  121. package/src/api/handler.ts +87 -604
  122. package/src/api/router.ts +15 -65
  123. package/src/api/service.ts +241 -0
  124. package/src/hooks/useAutoSave.ts +64 -0
  125. package/src/hooks/useCategories.ts +19 -47
  126. package/src/index.tsx +79 -293
  127. package/src/init.tsx +24 -11
  128. package/src/lib/blocks/BlockRenderer.tsx +1 -0
  129. package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
  130. package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
  131. package/src/lib/layouts/index.ts +4 -4
  132. package/src/lib/mappers/apiMapper.ts +63 -32
  133. package/src/lib/rich-text/RichTextEditor.tsx +16 -9
  134. package/src/lib/utils/config-resolver.ts +64 -0
  135. package/src/lib/utils/tree.ts +150 -0
  136. package/src/state/EditorContext.tsx +153 -232
  137. package/src/state/reducer.ts +141 -606
  138. package/src/state/types.ts +14 -1
  139. package/src/types/block.ts +10 -0
  140. package/src/types/post.ts +19 -1
  141. package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
  142. package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
  143. package/src/views/CanvasEditor/EditorBody.tsx +98 -610
  144. package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
  145. package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
  146. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
  147. package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
  148. package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
  149. package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
  150. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
  151. package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
  152. package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
  153. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
  154. package/src/views/CanvasEditor/components/index.ts +2 -1
  155. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
  156. package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
  157. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
  158. package/src/views/PostManager/EmptyState.tsx +9 -10
  159. package/src/views/PostManager/FilterDropdown.tsx +95 -0
  160. package/src/views/PostManager/LanguageFlags.tsx +6 -2
  161. package/src/views/PostManager/PostCards.tsx +127 -133
  162. package/src/views/PostManager/PostFilters.tsx +73 -68
  163. package/src/views/PostManager/PostManagerView.tsx +132 -179
  164. package/src/views/PostManager/PostStats.tsx +21 -20
  165. package/src/views/PostManager/PostTable.tsx +137 -165
  166. package/src/views/Settings/SettingsView.tsx +64 -180
  167. package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
  168. package/src/hooks/index.d.ts +0 -8
  169. package/src/hooks/index.d.ts.map +0 -1
  170. package/src/hooks/useBlog.d.ts +0 -31
  171. package/src/hooks/useBlog.d.ts.map +0 -1
  172. package/src/hooks/useBlogs.d.ts +0 -39
  173. package/src/hooks/useBlogs.d.ts.map +0 -1
  174. package/src/hooks/useCategories.d.ts +0 -9
  175. package/src/hooks/useCategories.d.ts.map +0 -1
  176. package/src/lib/blocks/BlockRenderer.d.ts +0 -54
  177. package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
  178. package/src/lib/config-storage.d.ts +0 -30
  179. package/src/lib/config-storage.d.ts.map +0 -1
  180. package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
  181. package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
  182. package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
  183. package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
  184. package/src/lib/layouts/index.d.ts +0 -23
  185. package/src/lib/layouts/index.d.ts.map +0 -1
  186. package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
  187. package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
  188. package/src/lib/mappers/apiMapper.d.ts +0 -66
  189. package/src/lib/mappers/apiMapper.d.ts.map +0 -1
  190. package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
  191. package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
  192. package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
  193. package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
  194. package/src/lib/rich-text/index.d.ts +0 -9
  195. package/src/lib/rich-text/index.d.ts.map +0 -1
  196. package/src/lib/utils/blockHelpers.d.ts +0 -23
  197. package/src/lib/utils/blockHelpers.d.ts.map +0 -1
  198. package/src/lib/utils/configValidation.d.ts +0 -23
  199. package/src/lib/utils/configValidation.d.ts.map +0 -1
  200. package/src/registry/BlockRegistry.d.ts +0 -62
  201. package/src/registry/BlockRegistry.d.ts.map +0 -1
  202. package/src/registry/index.d.ts +0 -6
  203. package/src/registry/index.d.ts.map +0 -1
  204. package/src/state/EditorContext.d.ts +0 -45
  205. package/src/state/EditorContext.d.ts.map +0 -1
  206. package/src/state/index.d.ts +0 -7
  207. package/src/state/index.d.ts.map +0 -1
  208. package/src/state/reducer.d.ts +0 -11
  209. package/src/state/reducer.d.ts.map +0 -1
  210. package/src/state/types.d.ts +0 -162
  211. package/src/state/types.d.ts.map +0 -1
  212. package/src/types/block.d.ts +0 -221
  213. package/src/types/block.d.ts.map +0 -1
  214. package/src/types/index.d.ts +0 -8
  215. package/src/types/index.d.ts.map +0 -1
  216. package/src/types/post.d.ts +0 -136
  217. package/src/types/post.d.ts.map +0 -1
  218. package/src/utils/client.d.ts +0 -48
  219. package/src/utils/client.d.ts.map +0 -1
  220. package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
  221. package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
  222. package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
  223. package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
  224. package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
  225. package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
  226. package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
  227. package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
  228. package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
  229. package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
  230. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
  231. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
  232. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
  233. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
  234. package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
  235. package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
  236. package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
  237. package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
  238. package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
  239. package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
  240. package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
  241. package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
  242. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
  243. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
  244. package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
  245. package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
  246. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
  247. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
  248. package/src/views/CanvasEditor/components/index.d.ts +0 -21
  249. package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
  250. package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
  251. package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
  252. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
  253. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
  254. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
  255. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
  256. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
  257. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
  258. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
  259. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
  260. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
  261. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
  262. package/src/views/CanvasEditor/index.d.ts +0 -16
  263. package/src/views/CanvasEditor/index.d.ts.map +0 -1
  264. package/src/views/PostManager/EmptyState.d.ts +0 -10
  265. package/src/views/PostManager/EmptyState.d.ts.map +0 -1
  266. package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
  267. package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
  268. package/src/views/PostManager/PostCards.d.ts +0 -15
  269. package/src/views/PostManager/PostCards.d.ts.map +0 -1
  270. package/src/views/PostManager/PostFilters.d.ts +0 -16
  271. package/src/views/PostManager/PostFilters.d.ts.map +0 -1
  272. package/src/views/PostManager/PostManagerView.d.ts +0 -11
  273. package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
  274. package/src/views/PostManager/PostStats.d.ts +0 -11
  275. package/src/views/PostManager/PostStats.d.ts.map +0 -1
  276. package/src/views/PostManager/PostTable.d.ts +0 -15
  277. package/src/views/PostManager/PostTable.d.ts.map +0 -1
  278. package/src/views/PostManager/index.d.ts +0 -12
  279. package/src/views/PostManager/index.d.ts.map +0 -1
  280. package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
  281. package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
  282. package/src/views/Preview/index.d.ts +0 -6
  283. package/src/views/Preview/index.d.ts.map +0 -1
  284. package/src/views/Settings/SettingsView.d.ts +0 -10
  285. package/src/views/Settings/SettingsView.d.ts.map +0 -1
  286. package/src/views/Settings/index.d.ts +0 -6
  287. package/src/views/Settings/index.d.ts.map +0 -1
  288. package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
  289. package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
  290. package/src/views/SlugSEO/index.d.ts +0 -6
  291. 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
- setUsers(data);
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="pt-8 border-t border-neutral-200 dark:border-neutral-800">
80
- <div className="flex items-center gap-3 mb-6">
81
- <Shield size={14} className="text-neutral-500 dark:text-neutral-400" />
82
- <label className="text-[10px] uppercase tracking-[0.2em] text-neutral-500 dark:text-neutral-400 font-black">
83
- Privacy Settings
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
- {/* Private Toggle */}
88
- <div className="flex items-center justify-between">
89
- <div>
90
- <label className="text-[10px] text-neutral-700 dark:text-neutral-300 font-bold block mb-1">
91
- Make Private
92
- </label>
93
- <p className="text-[9px] text-neutral-500 dark:text-neutral-400">
94
- Hide from public view
95
- </p>
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 Protection */}
94
+ {/* 2. Password & Users (Only if Private) */}
108
95
  {privacy?.isPrivate && (
109
- <div className="space-y-2">
110
- <div className="flex items-center justify-between">
111
- <div>
112
- <label className="text-[10px] text-neutral-700 dark:text-neutral-300 font-bold block mb-1">
113
- Password Protection
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="Enter password"
140
- 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"
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
- {/* Share with Users */}
147
- {privacy?.isPrivate && (
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-1.5 rounded-lg transition-colors ${(privacy.sharedWithUsers?.length || 0) > 0 || showUserSelector
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
- <Users size={14} />
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
- </div>
168
- {showUserSelector && (
169
- <div className="bg-dashboard-bg rounded-lg p-3 border border-dashboard-border max-h-48 overflow-y-auto">
170
- {loadingUsers ? (
171
- <p className="text-[10px] text-neutral-500 dark:text-neutral-400">Loading users...</p>
172
- ) : users.length === 0 ? (
173
- <p className="text-[10px] text-neutral-500 dark:text-neutral-400">No users found</p>
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-neutral-300 dark:border-neutral-700 text-primary focus:ring-primary"
139
+ className="size-4 rounded border-dashboard-border text-primary focus:ring-primary/20 bg-dashboard-card"
186
140
  />
187
- <div className="flex-1 min-w-0">
188
- <p className="text-[10px] font-bold text-neutral-700 dark:text-neutral-300 truncate">
189
- {user.name}
190
- </p>
191
- <p className="text-[9px] text-neutral-500 dark:text-neutral-400 truncate">
192
- {user.email}
193
- </p>
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
- </div>
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
- // Hero image and featured image are completely independent - no syncing
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
- image: heroData.image, // Use default image, not featured image
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 stateSummary = state.metadata.excerpt || '';
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
- const summaryMismatch = currentSummary !== stateSummary;
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
- // Only update title, summary, and category - NEVER update image
82
- // The hero block image should come from contentBlocks, not from featured image
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 || (heroBlock.data as any)?.title || '',
89
- summary: stateSummary || (heroBlock.data as any)?.summary || '',
90
- // DO NOT sync image - keep hero block's own image
91
- category: stateCategory || (heroBlock.data as any)?.category || '',
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
- const lang = languageRef.current;
25
- const url = lang
26
- ? `/api/plugin-blog/${postId}?language=${lang}`
27
- : `/api/plugin-blog/${postId}`;
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('Failed to load post');
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('Failed to load post:', 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
- // Only re-run on initial load (postId / currentPostId change), NOT on language change
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
- console.log('[useUnsavedChanges] Auto-save preference updated:', enabled);
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
- console.log('[useUnsavedChanges] Initialized saved state reference after post load');
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
- console.log('[useUnsavedChanges] Auto-saving...');
139
+
140
140
  await onSave(heroBlock);
141
141
  lastSavedStateRef.current = getStateSnapshot();
142
142
  setSaveStatus('saved');
143
- console.log('[useUnsavedChanges] Auto-save completed');
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
- * Botanical-themed empty state for when no posts are found
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 { Sprout, Plus } from 'lucide-react';
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-green-500/10 dark:bg-green-500/20 flex items-center justify-center mb-6">
20
- <Sprout className="text-green-600 dark:text-green-400 size-12" />
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 Posts Found' : 'No Posts Yet'}
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
- : 'Start growing your content garden. Create your first blog post to share your botanical knowledge with the world.'}
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-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20"
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
- Create Your First Post
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
- const langTitle = langData?.metadata?.title || post.title;
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={() => setHoveredLang(lang)}
72
+ onMouseEnter={() => {
73
+
74
+ setHoveredLang(lang);
75
+ }}
72
76
  onMouseLeave={() => setHoveredLang(null)}
73
77
  >
74
78
  <motion.div