@jhits/plugin-blog 0.0.14 → 0.0.16

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 (208) hide show
  1. package/package.json +5 -4
  2. package/src/api/categories.ts +43 -0
  3. package/src/api/check-title.ts +60 -0
  4. package/src/api/config-handler.ts +76 -0
  5. package/src/api/handler.ts +418 -0
  6. package/src/api/index.ts +33 -0
  7. package/src/api/route.ts +116 -0
  8. package/src/api/router.ts +128 -0
  9. package/src/api-server.ts +11 -0
  10. package/src/config.ts +161 -0
  11. package/src/hooks/index.d.ts +8 -0
  12. package/src/hooks/index.d.ts.map +1 -0
  13. package/src/hooks/index.ts +9 -0
  14. package/src/hooks/useBlog.d.ts +31 -0
  15. package/src/hooks/useBlog.d.ts.map +1 -0
  16. package/src/hooks/useBlog.ts +85 -0
  17. package/src/hooks/useBlogs.d.ts +39 -0
  18. package/src/hooks/useBlogs.d.ts.map +1 -0
  19. package/src/hooks/useBlogs.ts +123 -0
  20. package/src/hooks/useCategories.d.ts +9 -0
  21. package/src/hooks/useCategories.d.ts.map +1 -0
  22. package/src/hooks/useCategories.ts +76 -0
  23. package/src/index.server.ts +14 -0
  24. package/src/index.tsx +335 -0
  25. package/src/init.tsx +63 -0
  26. package/src/lib/blocks/BlockRenderer.d.ts +54 -0
  27. package/src/lib/blocks/BlockRenderer.d.ts.map +1 -0
  28. package/src/lib/blocks/BlockRenderer.tsx +141 -0
  29. package/src/lib/blocks/index.ts +6 -0
  30. package/src/lib/config-storage.d.ts +30 -0
  31. package/src/lib/config-storage.d.ts.map +1 -0
  32. package/src/lib/config-storage.ts +65 -0
  33. package/src/lib/index.ts +9 -0
  34. package/src/lib/layouts/blocks/ColumnsBlock.d.ts +25 -0
  35. package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -0
  36. package/src/lib/layouts/blocks/ColumnsBlock.tsx +298 -0
  37. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
  38. package/src/lib/layouts/blocks/SectionBlock.d.ts +25 -0
  39. package/src/lib/layouts/blocks/SectionBlock.d.ts.map +1 -0
  40. package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
  41. package/src/lib/layouts/blocks/index.ts +8 -0
  42. package/src/lib/layouts/index.d.ts +23 -0
  43. package/src/lib/layouts/index.d.ts.map +1 -0
  44. package/src/lib/layouts/index.ts +52 -0
  45. package/src/lib/layouts/registerLayoutBlocks.d.ts +9 -0
  46. package/src/lib/layouts/registerLayoutBlocks.d.ts.map +1 -0
  47. package/src/lib/layouts/registerLayoutBlocks.ts +64 -0
  48. package/src/lib/mappers/apiMapper.d.ts +66 -0
  49. package/src/lib/mappers/apiMapper.d.ts.map +1 -0
  50. package/src/lib/mappers/apiMapper.ts +254 -0
  51. package/src/lib/migration/index.ts +6 -0
  52. package/src/lib/migration/mapper.ts +140 -0
  53. package/src/lib/rich-text/RichTextEditor.d.ts +45 -0
  54. package/src/lib/rich-text/RichTextEditor.d.ts.map +1 -0
  55. package/src/lib/rich-text/RichTextEditor.tsx +826 -0
  56. package/src/lib/rich-text/RichTextPreview.d.ts +16 -0
  57. package/src/lib/rich-text/RichTextPreview.d.ts.map +1 -0
  58. package/src/lib/rich-text/RichTextPreview.tsx +210 -0
  59. package/src/lib/rich-text/index.d.ts +9 -0
  60. package/src/lib/rich-text/index.d.ts.map +1 -0
  61. package/src/lib/rich-text/index.ts +10 -0
  62. package/src/lib/utils/blockHelpers.d.ts +23 -0
  63. package/src/lib/utils/blockHelpers.d.ts.map +1 -0
  64. package/src/lib/utils/blockHelpers.ts +72 -0
  65. package/src/lib/utils/configValidation.d.ts +23 -0
  66. package/src/lib/utils/configValidation.d.ts.map +1 -0
  67. package/src/lib/utils/configValidation.ts +137 -0
  68. package/src/lib/utils/index.ts +8 -0
  69. package/src/lib/utils/slugify.ts +79 -0
  70. package/src/registry/BlockRegistry.d.ts +62 -0
  71. package/src/registry/BlockRegistry.d.ts.map +1 -0
  72. package/src/registry/BlockRegistry.ts +139 -0
  73. package/src/registry/index.d.ts +6 -0
  74. package/src/registry/index.d.ts.map +1 -0
  75. package/src/registry/index.ts +11 -0
  76. package/src/state/EditorContext.d.ts +45 -0
  77. package/src/state/EditorContext.d.ts.map +1 -0
  78. package/src/state/EditorContext.tsx +283 -0
  79. package/src/state/index.d.ts +7 -0
  80. package/src/state/index.d.ts.map +1 -0
  81. package/src/state/index.ts +8 -0
  82. package/src/state/reducer.d.ts +11 -0
  83. package/src/state/reducer.d.ts.map +1 -0
  84. package/src/state/reducer.ts +694 -0
  85. package/src/state/types.d.ts +162 -0
  86. package/src/state/types.d.ts.map +1 -0
  87. package/src/state/types.ts +160 -0
  88. package/src/types/block.d.ts +221 -0
  89. package/src/types/block.d.ts.map +1 -0
  90. package/src/types/block.ts +269 -0
  91. package/src/types/index.d.ts +8 -0
  92. package/src/types/index.d.ts.map +1 -0
  93. package/src/types/index.ts +17 -0
  94. package/src/types/post.d.ts +136 -0
  95. package/src/types/post.d.ts.map +1 -0
  96. package/src/types/post.ts +169 -0
  97. package/src/utils/client.d.ts +48 -0
  98. package/src/utils/client.d.ts.map +1 -0
  99. package/src/utils/client.ts +122 -0
  100. package/src/utils/index.ts +7 -0
  101. package/src/views/CanvasEditor/BlockWrapper.d.ts +16 -0
  102. package/src/views/CanvasEditor/BlockWrapper.d.ts.map +1 -0
  103. package/src/views/CanvasEditor/BlockWrapper.tsx +522 -0
  104. package/src/views/CanvasEditor/CanvasEditorView.d.ts +14 -0
  105. package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -0
  106. package/src/views/CanvasEditor/CanvasEditorView.tsx +337 -0
  107. package/src/views/CanvasEditor/EditorBody.d.ts +22 -0
  108. package/src/views/CanvasEditor/EditorBody.d.ts.map +1 -0
  109. package/src/views/CanvasEditor/EditorBody.tsx +665 -0
  110. package/src/views/CanvasEditor/EditorHeader.d.ts +18 -0
  111. package/src/views/CanvasEditor/EditorHeader.d.ts.map +1 -0
  112. package/src/views/CanvasEditor/EditorHeader.tsx +268 -0
  113. package/src/views/CanvasEditor/LayoutContainer.d.ts +17 -0
  114. package/src/views/CanvasEditor/LayoutContainer.d.ts.map +1 -0
  115. package/src/views/CanvasEditor/LayoutContainer.tsx +322 -0
  116. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +13 -0
  117. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +1 -0
  118. package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
  119. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +14 -0
  120. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +1 -0
  121. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
  122. package/src/views/CanvasEditor/components/EditorCanvas.d.ts +29 -0
  123. package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -0
  124. package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
  125. package/src/views/CanvasEditor/components/EditorLibrary.d.ts +7 -0
  126. package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -0
  127. package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
  128. package/src/views/CanvasEditor/components/EditorSidebar.d.ts +13 -0
  129. package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -0
  130. package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
  131. package/src/views/CanvasEditor/components/ErrorBanner.d.ts +6 -0
  132. package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +1 -0
  133. package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
  134. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +25 -0
  135. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -0
  136. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +341 -0
  137. package/src/views/CanvasEditor/components/LibraryItem.d.ts +14 -0
  138. package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +1 -0
  139. package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
  140. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +15 -0
  141. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -0
  142. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
  143. package/src/views/CanvasEditor/components/index.d.ts +21 -0
  144. package/src/views/CanvasEditor/components/index.d.ts.map +1 -0
  145. package/src/views/CanvasEditor/components/index.ts +28 -0
  146. package/src/views/CanvasEditor/hooks/index.d.ts +10 -0
  147. package/src/views/CanvasEditor/hooks/index.d.ts.map +1 -0
  148. package/src/views/CanvasEditor/hooks/index.ts +10 -0
  149. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +8 -0
  150. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -0
  151. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
  152. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +3 -0
  153. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +1 -0
  154. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
  155. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +5 -0
  156. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -0
  157. package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
  158. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +2 -0
  159. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -0
  160. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
  161. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +25 -0
  162. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +1 -0
  163. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
  164. package/src/views/CanvasEditor/index.d.ts +16 -0
  165. package/src/views/CanvasEditor/index.d.ts.map +1 -0
  166. package/src/views/CanvasEditor/index.ts +16 -0
  167. package/src/views/PostManager/EmptyState.d.ts +10 -0
  168. package/src/views/PostManager/EmptyState.d.ts.map +1 -0
  169. package/src/views/PostManager/EmptyState.tsx +42 -0
  170. package/src/views/PostManager/PostActionsMenu.d.ts +12 -0
  171. package/src/views/PostManager/PostActionsMenu.d.ts.map +1 -0
  172. package/src/views/PostManager/PostActionsMenu.tsx +112 -0
  173. package/src/views/PostManager/PostCards.d.ts +15 -0
  174. package/src/views/PostManager/PostCards.d.ts.map +1 -0
  175. package/src/views/PostManager/PostCards.tsx +197 -0
  176. package/src/views/PostManager/PostFilters.d.ts +16 -0
  177. package/src/views/PostManager/PostFilters.d.ts.map +1 -0
  178. package/src/views/PostManager/PostFilters.tsx +95 -0
  179. package/src/views/PostManager/PostManagerView.d.ts +11 -0
  180. package/src/views/PostManager/PostManagerView.d.ts.map +1 -0
  181. package/src/views/PostManager/PostManagerView.tsx +289 -0
  182. package/src/views/PostManager/PostStats.d.ts +11 -0
  183. package/src/views/PostManager/PostStats.d.ts.map +1 -0
  184. package/src/views/PostManager/PostStats.tsx +81 -0
  185. package/src/views/PostManager/PostTable.d.ts +15 -0
  186. package/src/views/PostManager/PostTable.d.ts.map +1 -0
  187. package/src/views/PostManager/PostTable.tsx +230 -0
  188. package/src/views/PostManager/index.d.ts +12 -0
  189. package/src/views/PostManager/index.d.ts.map +1 -0
  190. package/src/views/PostManager/index.ts +15 -0
  191. package/src/views/Preview/PreviewBridgeView.d.ts +12 -0
  192. package/src/views/Preview/PreviewBridgeView.d.ts.map +1 -0
  193. package/src/views/Preview/PreviewBridgeView.tsx +64 -0
  194. package/src/views/Preview/index.d.ts +6 -0
  195. package/src/views/Preview/index.d.ts.map +1 -0
  196. package/src/views/Preview/index.ts +7 -0
  197. package/src/views/Settings/SettingsView.d.ts +10 -0
  198. package/src/views/Settings/SettingsView.d.ts.map +1 -0
  199. package/src/views/Settings/SettingsView.tsx +298 -0
  200. package/src/views/Settings/index.d.ts +6 -0
  201. package/src/views/Settings/index.d.ts.map +1 -0
  202. package/src/views/Settings/index.ts +7 -0
  203. package/src/views/SlugSEO/SlugSEOManagerView.d.ts +12 -0
  204. package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -0
  205. package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
  206. package/src/views/SlugSEO/index.d.ts +6 -0
  207. package/src/views/SlugSEO/index.d.ts.map +1 -0
  208. package/src/views/SlugSEO/index.ts +7 -0
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef } from 'react';
4
+
5
+ export interface LibraryItemProps {
6
+ icon: React.ReactNode;
7
+ label: string;
8
+ blockType: string;
9
+ description?: string;
10
+ onAddBlock?: (blockType: string) => void;
11
+ }
12
+
13
+ /**
14
+ * Reusable Library Item Component
15
+ * Makes blocks draggable from the library and clickable to add at bottom
16
+ */
17
+ export function LibraryItem({
18
+ icon,
19
+ label,
20
+ blockType,
21
+ description,
22
+ onAddBlock
23
+ }: LibraryItemProps) {
24
+ const [hasDragged, setHasDragged] = useState(false);
25
+ const mouseDownRef = useRef<{ x: number; y: number } | null>(null);
26
+
27
+ const handleDragStart = (e: React.DragEvent) => {
28
+ e.dataTransfer.setData('block-type', blockType);
29
+ e.dataTransfer.effectAllowed = 'move';
30
+ setHasDragged(true); // Mark as dragged when drag starts
31
+ };
32
+
33
+ const handleMouseDown = (e: React.MouseEvent) => {
34
+ // Track mouse position on mousedown
35
+ mouseDownRef.current = { x: e.clientX, y: e.clientY };
36
+ setHasDragged(false); // Reset drag state
37
+ };
38
+
39
+ const handleMouseMove = (e: React.MouseEvent) => {
40
+ // If mouse moved more than 5px, consider it a drag
41
+ if (mouseDownRef.current) {
42
+ const dx = Math.abs(e.clientX - mouseDownRef.current.x);
43
+ const dy = Math.abs(e.clientY - mouseDownRef.current.y);
44
+ if (dx > 5 || dy > 5) {
45
+ setHasDragged(true);
46
+ }
47
+ }
48
+ };
49
+
50
+ const handleClick = (e: React.MouseEvent) => {
51
+ // Only add block if we didn't drag
52
+ if (!hasDragged && onAddBlock) {
53
+ e.preventDefault();
54
+ e.stopPropagation();
55
+ onAddBlock(blockType);
56
+ }
57
+ // Reset state
58
+ mouseDownRef.current = null;
59
+ setTimeout(() => setHasDragged(false), 100);
60
+ };
61
+
62
+ return (
63
+ <div
64
+ draggable
65
+ onDragStart={handleDragStart}
66
+ onMouseDown={handleMouseDown}
67
+ onMouseMove={handleMouseMove}
68
+ onClick={handleClick}
69
+ className="flex flex-col items-center justify-center p-5 rounded-2xl border border-dashboard-border bg-dashboard-card hover:border-primary hover:shadow-xl hover:shadow-primary/5 transition-all cursor-pointer group"
70
+ >
71
+ <div className="text-neutral-400 dark:text-neutral-500 group-hover:text-primary dark:group-hover:text-primary mb-3 transition-colors duration-300">
72
+ {React.cloneElement(icon as React.ReactElement, { strokeWidth: 1.5 } as any)}
73
+ </div>
74
+ <span className="text-[9px] font-black uppercase tracking-[0.15em] text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-950 dark:group-hover:text-white transition-colors">
75
+ {label}
76
+ </span>
77
+ </div>
78
+ );
79
+ }
80
+
@@ -0,0 +1,15 @@
1
+ export interface PrivacySettings {
2
+ isPrivate?: boolean;
3
+ password?: string;
4
+ sharedWithUsers?: string[];
5
+ }
6
+ export interface PrivacySettingsSectionProps {
7
+ privacy?: PrivacySettings;
8
+ onUpdate: (privacy: PrivacySettings) => void;
9
+ }
10
+ /**
11
+ * Privacy Settings Section Component
12
+ * Handles privacy settings: private, password-protected, share with users
13
+ */
14
+ export declare function PrivacySettingsSection({ privacy, onUpdate, }: PrivacySettingsSectionProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=PrivacySettingsSection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrivacySettingsSection.d.ts","sourceRoot":"","sources":["PrivacySettingsSection.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,2BAA2B;IACxC,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACnC,OAAO,EACP,QAAQ,GACX,EAAE,2BAA2B,2CA2L7B"}
@@ -0,0 +1,212 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { Shield, Key, Users } from 'lucide-react';
5
+
6
+ export interface PrivacySettings {
7
+ isPrivate?: boolean;
8
+ password?: string;
9
+ sharedWithUsers?: string[];
10
+ }
11
+
12
+ export interface PrivacySettingsSectionProps {
13
+ privacy?: PrivacySettings;
14
+ onUpdate: (privacy: PrivacySettings) => void;
15
+ }
16
+
17
+ /**
18
+ * Privacy Settings Section Component
19
+ * Handles privacy settings: private, password-protected, share with users
20
+ */
21
+ export function PrivacySettingsSection({
22
+ privacy,
23
+ onUpdate,
24
+ }: PrivacySettingsSectionProps) {
25
+ const [users, setUsers] = useState<Array<{ _id: string; name: string; email: string }>>([]);
26
+ const [loadingUsers, setLoadingUsers] = useState(false);
27
+ const [showPasswordInput, setShowPasswordInput] = useState(false);
28
+ const [passwordValue, setPasswordValue] = useState(privacy?.password || '');
29
+ const [showUserSelector, setShowUserSelector] = useState(false);
30
+
31
+ // Fetch users from plugin-users
32
+ useEffect(() => {
33
+ const fetchUsers = async () => {
34
+ try {
35
+ setLoadingUsers(true);
36
+ const res = await fetch('/api/users');
37
+ 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
+ }
46
+ };
47
+ fetchUsers();
48
+ }, []);
49
+
50
+ const handlePrivacyToggle = (isPrivate: boolean) => {
51
+ onUpdate({
52
+ ...privacy,
53
+ isPrivate,
54
+ // Clear password and shared users if making public
55
+ ...(isPrivate ? {} : { password: undefined, sharedWithUsers: undefined }),
56
+ });
57
+ };
58
+
59
+ const handlePasswordChange = (password: string) => {
60
+ setPasswordValue(password);
61
+ onUpdate({
62
+ ...privacy,
63
+ password: password || undefined,
64
+ });
65
+ };
66
+
67
+ const handleUserToggle = (userId: string) => {
68
+ const currentUsers = privacy?.sharedWithUsers || [];
69
+ const newUsers = currentUsers.includes(userId)
70
+ ? currentUsers.filter(id => id !== userId)
71
+ : [...currentUsers, userId];
72
+ onUpdate({
73
+ ...privacy,
74
+ sharedWithUsers: newUsers.length > 0 ? newUsers : undefined,
75
+ });
76
+ };
77
+
78
+ 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
84
+ </label>
85
+ </div>
86
+ <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>
96
+ </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
+ </div>
106
+
107
+ {/* Password Protection */}
108
+ {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
114
+ </label>
115
+ <p className="text-[9px] text-neutral-500 dark:text-neutral-400">
116
+ Require password to view
117
+ </p>
118
+ </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
+ <input
136
+ type="password"
137
+ value={passwordValue}
138
+ 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"
141
+ />
142
+ )}
143
+ </div>
144
+ )}
145
+
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>
158
+ <button
159
+ 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
+ }`}
164
+ >
165
+ <Users size={14} />
166
+ </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
+ >
181
+ <input
182
+ type="checkbox"
183
+ checked={privacy.sharedWithUsers?.includes(user._id) || false}
184
+ onChange={() => handleUserToggle(user._id)}
185
+ className="rounded border-neutral-300 dark:border-neutral-700 text-primary focus:ring-primary"
186
+ />
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>
194
+ </div>
195
+ </label>
196
+ ))}
197
+ </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
+ )}
206
+ </div>
207
+ )}
208
+ </div>
209
+ </section>
210
+ );
211
+ }
212
+
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Canvas Editor Components
3
+ * Exports all components used in the Canvas Editor
4
+ */
5
+ export { LibraryItem } from './LibraryItem';
6
+ export type { LibraryItemProps } from './LibraryItem';
7
+ export { CustomBlockItem } from './CustomBlockItem';
8
+ export type { CustomBlockItemProps } from './CustomBlockItem';
9
+ export { FeaturedMediaSection } from './FeaturedMediaSection';
10
+ export type { FeaturedMediaSectionProps, FeaturedImage } from './FeaturedMediaSection';
11
+ export { PrivacySettingsSection } from './PrivacySettingsSection';
12
+ export type { PrivacySettingsSectionProps, PrivacySettings } from './PrivacySettingsSection';
13
+ export { ErrorBanner } from './ErrorBanner';
14
+ export type { ErrorBannerProps } from './ErrorBanner';
15
+ export { EditorLibrary } from './EditorLibrary';
16
+ export type { EditorLibraryProps } from './EditorLibrary';
17
+ export { EditorCanvas } from './EditorCanvas';
18
+ export type { EditorCanvasProps } from './EditorCanvas';
19
+ export { EditorSidebar } from './EditorSidebar';
20
+ export type { EditorSidebarProps } from './EditorSidebar';
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,YAAY,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE7F,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Canvas Editor Components
3
+ * Exports all components used in the Canvas Editor
4
+ */
5
+
6
+ export { LibraryItem } from './LibraryItem';
7
+ export type { LibraryItemProps } from './LibraryItem';
8
+
9
+ export { CustomBlockItem } from './CustomBlockItem';
10
+ export type { CustomBlockItemProps } from './CustomBlockItem';
11
+
12
+ export { FeaturedMediaSection } from './FeaturedMediaSection';
13
+ export type { FeaturedMediaSectionProps, FeaturedImage } from './FeaturedMediaSection';
14
+
15
+ export { PrivacySettingsSection } from './PrivacySettingsSection';
16
+ export type { PrivacySettingsSectionProps, PrivacySettings } from './PrivacySettingsSection';
17
+
18
+ export { ErrorBanner } from './ErrorBanner';
19
+ export type { ErrorBannerProps } from './ErrorBanner';
20
+
21
+ export { EditorLibrary } from './EditorLibrary';
22
+ export type { EditorLibraryProps } from './EditorLibrary';
23
+
24
+ export { EditorCanvas } from './EditorCanvas';
25
+ export type { EditorCanvasProps } from './EditorCanvas';
26
+
27
+ export { EditorSidebar } from './EditorSidebar';
28
+ export type { EditorSidebarProps } from './EditorSidebar';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canvas Editor Hooks
3
+ * Exports all custom hooks used in the Canvas Editor
4
+ */
5
+ export { usePostLoader } from './usePostLoader';
6
+ export { useHeroBlock } from './useHeroBlock';
7
+ export { useRegisteredBlocks } from './useRegisteredBlocks';
8
+ export { useKeyboardShortcuts } from './useKeyboardShortcuts';
9
+ export { useUnsavedChanges } from './useUnsavedChanges';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Canvas Editor Hooks
3
+ * Exports all custom hooks used in the Canvas Editor
4
+ */
5
+
6
+ export { usePostLoader } from './usePostLoader';
7
+ export { useHeroBlock } from './useHeroBlock';
8
+ export { useRegisteredBlocks } from './useRegisteredBlocks';
9
+ export { useKeyboardShortcuts } from './useKeyboardShortcuts';
10
+ export { useUnsavedChanges } from './useUnsavedChanges';
@@ -0,0 +1,8 @@
1
+ import type { Block } from '../../../types/block';
2
+ import type { EditorState } from '../../../state/types';
3
+ export declare function useHeroBlock(state: EditorState, registeredBlocks: any[]): {
4
+ heroBlock: Block | null;
5
+ setHeroBlock: import("react").Dispatch<import("react").SetStateAction<Block | null>>;
6
+ heroBlockDefinition: import("../../..").BlockTypeDefinition | undefined;
7
+ };
8
+ //# sourceMappingURL=useHeroBlock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHeroBlock.d.ts","sourceRoot":"","sources":["useHeroBlock.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAUxD,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,EAAE;;;;EAyFvE"}
@@ -0,0 +1,103 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { blockRegistry } from '../../../registry/BlockRegistry';
3
+ import type { Block } from '../../../types/block';
4
+ import type { EditorState } from '../../../state/types';
5
+
6
+ // Generate a unique block ID
7
+ function generateBlockId(): string {
8
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
9
+ return crypto.randomUUID();
10
+ }
11
+ return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
12
+ }
13
+
14
+ export function useHeroBlock(state: EditorState, registeredBlocks: any[]) {
15
+ const [heroBlock, setHeroBlock] = useState<Block | null>(null);
16
+
17
+ // Get Hero block definition from registered blocks (REQUIRED)
18
+ const heroBlockDefinition = useMemo(() => {
19
+ return blockRegistry.get('hero');
20
+ }, [registeredBlocks]);
21
+
22
+ // Initialize hero block if Hero block definition exists
23
+ useEffect(() => {
24
+ if (heroBlockDefinition) {
25
+ // Get default data from block definition
26
+ const heroData = heroBlockDefinition.defaultData || {};
27
+
28
+ // Initialize hero block only if it doesn't exist yet
29
+ setHeroBlock(prev => {
30
+ if (!prev) {
31
+ // First, try to find hero block in contentBlocks (from loaded post)
32
+ const heroBlockFromContent = state.blocks.find(block => block.type === 'hero');
33
+
34
+ if (heroBlockFromContent) {
35
+ return heroBlockFromContent;
36
+ }
37
+
38
+ // If no hero block in contentBlocks, initialize from defaults
39
+ // Hero image and featured image are completely independent - no syncing
40
+ const initialData = {
41
+ ...heroData,
42
+ title: state.title || heroData.title || '',
43
+ summary: state.metadata.excerpt || heroData.summary || '',
44
+ image: heroData.image, // Use default image, not featured image
45
+ };
46
+ return {
47
+ id: generateBlockId(),
48
+ type: 'hero',
49
+ data: initialData,
50
+ };
51
+ }
52
+ // Keep existing hero block data - let the Edit component manage it
53
+ return prev;
54
+ });
55
+ } else {
56
+ setHeroBlock(null);
57
+ }
58
+ }, [heroBlockDefinition, state.blocks, state.title, state.metadata.excerpt]);
59
+
60
+ // 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
+ useEffect(() => {
64
+ 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
+ const currentTitle = (heroBlock.data as any)?.title || '';
68
+ const currentSummary = (heroBlock.data as any)?.summary || '';
69
+ const currentCategory = (heroBlock.data as any)?.category || '';
70
+
71
+ const stateTitle = state.title || '';
72
+ const stateSummary = state.metadata.excerpt || '';
73
+ const stateCategory = state.metadata.categories?.[0] || '';
74
+
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
+ const titleMismatch = currentTitle !== stateTitle;
78
+ const summaryMismatch = currentSummary !== stateSummary;
79
+ const categoryMismatch = currentCategory !== stateCategory;
80
+
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)) {
84
+ setHeroBlock({
85
+ ...heroBlock,
86
+ data: {
87
+ ...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 || '',
92
+ },
93
+ });
94
+ }
95
+ }
96
+ }, [state.postId, state.title, state.metadata.excerpt, state.metadata.categories, heroBlockDefinition, heroBlock]);
97
+
98
+ return {
99
+ heroBlock,
100
+ setHeroBlock,
101
+ heroBlockDefinition,
102
+ };
103
+ }
@@ -0,0 +1,3 @@
1
+ import type { EditorState } from '../../../state/types';
2
+ export declare function useKeyboardShortcuts(state: EditorState, dispatch: (action: any) => void, canUndo: boolean, canRedo: boolean, undo: () => void, redo: () => void): void;
3
+ //# sourceMappingURL=useKeyboardShortcuts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useKeyboardShortcuts.d.ts","sourceRoot":"","sources":["useKeyboardShortcuts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAUxD,wBAAgB,oBAAoB,CAChC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,EAC/B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,IAAI,EAChB,IAAI,EAAE,MAAM,IAAI,QA2HnB"}
@@ -0,0 +1,142 @@
1
+ import { useEffect } from 'react';
2
+ import type { Block } from '../../../types/block';
3
+ import type { EditorState } from '../../../state/types';
4
+
5
+ // Generate a unique block ID
6
+ function generateBlockId(): string {
7
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
8
+ return crypto.randomUUID();
9
+ }
10
+ return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
11
+ }
12
+
13
+ export function useKeyboardShortcuts(
14
+ state: EditorState,
15
+ dispatch: (action: any) => void,
16
+ canUndo: boolean,
17
+ canRedo: boolean,
18
+ undo: () => void,
19
+ redo: () => void
20
+ ) {
21
+ useEffect(() => {
22
+ const handleKeyDown = (e: KeyboardEvent) => {
23
+ // Don't handle shortcuts if user is typing in an input/textarea/contentEditable
24
+ const target = e.target as HTMLElement;
25
+ const isEditableElement = target.tagName === 'INPUT' ||
26
+ target.tagName === 'TEXTAREA' ||
27
+ target.isContentEditable ||
28
+ target.closest('input, textarea, [contenteditable="true"]');
29
+
30
+ // Check for Ctrl+Z / Cmd+Z (Undo)
31
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
32
+ if (!isEditableElement) {
33
+ e.preventDefault();
34
+ e.stopPropagation();
35
+ if (canUndo) {
36
+ undo();
37
+ }
38
+ return;
39
+ }
40
+ }
41
+
42
+ // Check for Ctrl+Shift+Z / Cmd+Shift+Z or Ctrl+Y / Cmd+Y (Redo)
43
+ if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') || ((e.ctrlKey || e.metaKey) && e.key === 'y')) {
44
+ if (!isEditableElement) {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ if (canRedo) {
48
+ redo();
49
+ }
50
+ return;
51
+ }
52
+ }
53
+
54
+ // Check for Ctrl+V (Windows/Linux) or Cmd+V (Mac)
55
+ if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
56
+ // Don't paste if user is typing in an input/textarea/contentEditable
57
+ const target = e.target as HTMLElement;
58
+ const isEditableElement = target.tagName === 'INPUT' ||
59
+ target.tagName === 'TEXTAREA' ||
60
+ target.isContentEditable ||
61
+ target.closest('input, textarea, [contenteditable="true"]');
62
+
63
+ if (isEditableElement) {
64
+ return; // Let the browser handle paste in editable elements
65
+ }
66
+
67
+ // Check if there's a copied block
68
+ if (typeof window !== 'undefined') {
69
+ const copiedBlockJson = localStorage.getItem('__BLOG_EDITOR_COPIED_BLOCK__');
70
+ if (copiedBlockJson) {
71
+ try {
72
+ e.preventDefault();
73
+ e.stopPropagation();
74
+
75
+ const copiedBlock = JSON.parse(copiedBlockJson) as Block;
76
+
77
+ // Clone a block with new IDs (recursive for nested blocks)
78
+ const cloneBlock = (blockToClone: Block): Block => {
79
+ const cloned: Block = {
80
+ ...blockToClone,
81
+ id: generateBlockId(),
82
+ data: { ...blockToClone.data },
83
+ meta: blockToClone.meta ? { ...blockToClone.meta } : undefined,
84
+ };
85
+
86
+ // Handle children if they exist
87
+ if (blockToClone.children) {
88
+ if (Array.isArray(blockToClone.children) && blockToClone.children.length > 0) {
89
+ if (typeof blockToClone.children[0] === 'object') {
90
+ cloned.children = (blockToClone.children as Block[]).map(cloneBlock);
91
+ } else {
92
+ // If children are IDs, find and clone the actual blocks
93
+ const allBlocks = state.blocks;
94
+ const childIds = blockToClone.children as string[];
95
+ const childBlocks = childIds
96
+ .map((childId: string) => allBlocks.find(b => b.id === childId))
97
+ .filter((b): b is Block => b !== undefined);
98
+ cloned.children = childBlocks.map(cloneBlock);
99
+ }
100
+ }
101
+ }
102
+
103
+ return cloned;
104
+ };
105
+
106
+ const pastedBlock = cloneBlock(copiedBlock);
107
+
108
+ // Find where to paste - use hovered block or selected block, or paste at end
109
+ const hoveredBlockId = (window as any).__BLOG_EDITOR_HOVERED_BLOCK_ID__;
110
+ const targetBlockId = hoveredBlockId || state.selectedBlockId;
111
+
112
+ let pasteIndex: number | undefined;
113
+ if (targetBlockId) {
114
+ const targetIndex = state.blocks.findIndex(b => b.id === targetBlockId);
115
+ if (targetIndex !== -1) {
116
+ pasteIndex = targetIndex + 1;
117
+ }
118
+ }
119
+
120
+ // Dispatch ADD_BLOCK with the full block structure
121
+ dispatch({
122
+ type: 'ADD_BLOCK',
123
+ payload: {
124
+ block: pastedBlock,
125
+ index: pasteIndex,
126
+ containerId: undefined
127
+ }
128
+ });
129
+ } catch (error) {
130
+ console.error('Failed to paste block:', error);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ };
136
+
137
+ window.addEventListener('keydown', handleKeyDown);
138
+ return () => {
139
+ window.removeEventListener('keydown', handleKeyDown);
140
+ };
141
+ }, [state.blocks, state.selectedBlockId, dispatch, canUndo, canRedo, undo, redo]);
142
+ }