@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,112 @@
1
+ /**
2
+ * Post Actions Menu Component
3
+ * Three-dot menu with actions for each post
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useState, useRef, useEffect } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import { MoreVertical, Edit, Eye, Copy, Trash2 } from 'lucide-react';
11
+
12
+ export interface PostActionsMenuProps {
13
+ onEdit: () => void;
14
+ onPreview: () => void;
15
+ onDuplicate: () => void;
16
+ onDelete: () => void;
17
+ }
18
+
19
+ export function PostActionsMenu({
20
+ onEdit,
21
+ onPreview,
22
+ onDuplicate,
23
+ onDelete,
24
+ }: PostActionsMenuProps) {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const [menuPosition, setMenuPosition] = useState({ top: 0, right: 0 });
27
+ const buttonRef = useRef<HTMLButtonElement>(null);
28
+ const menuRef = useRef<HTMLDivElement>(null);
29
+
30
+ // Calculate menu position when opening
31
+ useEffect(() => {
32
+ if (isOpen && buttonRef.current) {
33
+ const buttonRect = buttonRef.current.getBoundingClientRect();
34
+ setMenuPosition({
35
+ top: buttonRect.bottom + 8, // 8px = mt-2 equivalent
36
+ right: window.innerWidth - buttonRect.right,
37
+ });
38
+ }
39
+ }, [isOpen]);
40
+
41
+ useEffect(() => {
42
+ function handleClickOutside(event: MouseEvent) {
43
+ if (
44
+ menuRef.current &&
45
+ !menuRef.current.contains(event.target as Node) &&
46
+ buttonRef.current &&
47
+ !buttonRef.current.contains(event.target as Node)
48
+ ) {
49
+ setIsOpen(false);
50
+ }
51
+ }
52
+
53
+ if (isOpen) {
54
+ document.addEventListener('mousedown', handleClickOutside);
55
+ }
56
+
57
+ return () => {
58
+ document.removeEventListener('mousedown', handleClickOutside);
59
+ };
60
+ }, [isOpen]);
61
+
62
+ const actions = [
63
+ { label: 'Edit', icon: Edit, onClick: onEdit, color: 'text-neutral-600 dark:text-neutral-400' },
64
+ // { label: 'Preview', icon: Eye, onClick: onPreview, color: 'text-blue-600 dark:text-blue-400' },
65
+ { label: 'Duplicate', icon: Copy, onClick: onDuplicate, color: 'text-neutral-600 dark:text-neutral-400' },
66
+ { label: 'Delete', icon: Trash2, onClick: onDelete, color: 'text-red-600 dark:text-red-400' },
67
+ ];
68
+
69
+ const menuContent = isOpen && (
70
+ <div
71
+ ref={menuRef}
72
+ className="fixed w-48 bg-dashboard-card border border-dashboard-border rounded-2xl shadow-xl z-9999 overflow-hidden"
73
+ style={{
74
+ top: `${menuPosition.top}px`,
75
+ right: `${menuPosition.right}px`,
76
+ }}
77
+ >
78
+ {actions.map((action) => {
79
+ const Icon = action.icon;
80
+ return (
81
+ <button
82
+ key={action.label}
83
+ onClick={() => {
84
+ action.onClick();
85
+ setIsOpen(false);
86
+ }}
87
+ className={`w-full flex items-center font-sans gap-3 px-4 py-3 text-sm hover:bg-dashboard-bg transition-colors ${action.color}`}
88
+ >
89
+ <Icon size={16} />
90
+ <span>{action.label}</span>
91
+ </button>
92
+ );
93
+ })}
94
+ </div>
95
+ );
96
+
97
+ return (
98
+ <>
99
+ <button
100
+ ref={buttonRef}
101
+ onClick={() => setIsOpen(!isOpen)}
102
+ className="p-2 text-neutral-400 hover:text-dashboard-text hover:bg-dashboard-bg rounded-full transition-colors"
103
+ title="Actions"
104
+ >
105
+ <MoreVertical size={18} />
106
+ </button>
107
+
108
+ {typeof window !== 'undefined' && isOpen && createPortal(menuContent, document.body)}
109
+ </>
110
+ );
111
+ }
112
+
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Post Cards Component
3
+ * Card-based layout for displaying posts
4
+ */
5
+ import { PostListItem } from '../../types/post';
6
+ export interface PostCardsProps {
7
+ posts: PostListItem[];
8
+ locale: string;
9
+ onEdit: (postId: string) => void;
10
+ onPreview: (postId: string) => void;
11
+ onDuplicate: (postId: string) => void;
12
+ onDelete: (postId: string) => void;
13
+ }
14
+ export declare function PostCards({ posts, locale, onEdit, onPreview, onDuplicate, onDelete, }: PostCardsProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=PostCards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostCards.d.ts","sourceRoot":"","sources":["PostCards.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAI5D,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AA0BD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,MAAM,EACN,MAAM,EACN,SAAS,EACT,WAAW,EACX,QAAQ,GACX,EAAE,cAAc,2CA6IhB"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Post Cards Component
3
+ * Card-based layout for displaying posts
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React, { useState, useEffect } from 'react';
9
+ import { Calendar, User, UserCheck } from 'lucide-react';
10
+ import { Image } from '@jhits/plugin-images';
11
+ import { PostListItem, PostStatus } from '../../types/post';
12
+ import { PostActionsMenu } from './PostActionsMenu';
13
+ import { useSession } from 'next-auth/react';
14
+
15
+ export interface PostCardsProps {
16
+ posts: PostListItem[];
17
+ locale: string;
18
+ onEdit: (postId: string) => void;
19
+ onPreview: (postId: string) => void;
20
+ onDuplicate: (postId: string) => void;
21
+ onDelete: (postId: string) => void;
22
+ }
23
+
24
+ function getStatusBadgeColor(status: PostStatus) {
25
+ switch (status) {
26
+ case 'published':
27
+ return 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20';
28
+ case 'draft':
29
+ return 'bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/20';
30
+ case 'scheduled':
31
+ return 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20';
32
+ case 'archived':
33
+ return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
34
+ default:
35
+ return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
36
+ }
37
+ }
38
+
39
+ function formatDate(dateString: string | undefined, locale: string) {
40
+ if (!dateString) return 'No date';
41
+ return new Date(dateString).toLocaleDateString(locale, {
42
+ day: 'numeric',
43
+ month: 'short',
44
+ year: 'numeric',
45
+ });
46
+ }
47
+
48
+ export function PostCards({
49
+ posts,
50
+ locale,
51
+ onEdit,
52
+ onPreview,
53
+ onDuplicate,
54
+ onDelete,
55
+ }: PostCardsProps) {
56
+ const { data: session, status: sessionStatus } = useSession();
57
+ const currentUserId = (session?.user as any)?.id;
58
+ const [userMap, setUserMap] = useState<Record<string, string>>({});
59
+
60
+ // Helper function to check if user is the owner
61
+ const isPostOwner = (post: PostListItem): boolean => {
62
+ if (sessionStatus === 'loading') return false; // Don't show actions while loading
63
+ if (!currentUserId || !post.authorId) return false;
64
+ // Convert both to strings for comparison to handle ObjectId vs string
65
+ return String(currentUserId) === String(post.authorId);
66
+ };
67
+
68
+ // Fetch users to map IDs to names
69
+ useEffect(() => {
70
+ const fetchUsers = async () => {
71
+ try {
72
+ const response = await fetch('/api/users');
73
+ const users = await response.json();
74
+ if (Array.isArray(users)) {
75
+ const map: Record<string, string> = {};
76
+ users.forEach((user: { _id: string; name?: string; email?: string }) => {
77
+ const id = user._id?.toString();
78
+ if (id) {
79
+ map[id] = user.name || user.email || 'Unknown';
80
+ }
81
+ });
82
+ setUserMap(map);
83
+ }
84
+ } catch (error) {
85
+ console.error('Failed to fetch users:', error);
86
+ }
87
+ };
88
+ fetchUsers();
89
+ }, []);
90
+
91
+ const getAuthorName = (authorId?: string) => {
92
+ if (!authorId) return 'Unknown';
93
+ return userMap[authorId] || authorId;
94
+ };
95
+
96
+ return (
97
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
98
+ {posts.map((post) => (
99
+ <div
100
+ key={post.id}
101
+ className="bg-dashboard-card rounded-2xl border border-dashboard-border overflow-hidden hover:shadow-xl transition-all duration-300 group"
102
+ >
103
+ {/* Featured Image */}
104
+ <div className="relative w-full h-48 bg-neutral-200 dark:bg-neutral-800 overflow-hidden">
105
+ {post.featuredImage ? (
106
+ <Image
107
+ id={post.featuredImage}
108
+ alt={post.title}
109
+ fill
110
+ editable={false}
111
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
112
+ />
113
+ ) : (
114
+ <div className="w-full h-full flex items-center justify-center">
115
+ <span className="text-sm text-neutral-400">No Image</span>
116
+ </div>
117
+ )}
118
+ {/* Actions Menu - Top Left - Only show for own posts */}
119
+ {isPostOwner(post) && (
120
+ <div className="absolute top-4 left-4 z-10">
121
+ <div className="bg-white/90 dark:bg-neutral-900/90 backdrop-blur-sm rounded-full p-1 shadow-lg border border-neutral-200 dark:border-neutral-700">
122
+ <PostActionsMenu
123
+ onEdit={() => onEdit(post.id)}
124
+ onPreview={() => onPreview(post.id)}
125
+ onDuplicate={() => onDuplicate(post.id)}
126
+ onDelete={() => onDelete(post.id)}
127
+ />
128
+ </div>
129
+ </div>
130
+ )}
131
+ {/* Status Badge Overlay */}
132
+ <div className="absolute top-4 right-4">
133
+ <span
134
+ className={`inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider border backdrop-blur-sm ${getStatusBadgeColor(post.status)}`}
135
+ >
136
+ {post.status}
137
+ </span>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Card Content */}
142
+ <div className="p-6">
143
+ {/* Title & Slug */}
144
+ <div className="mb-4">
145
+ <button
146
+ onClick={() => onEdit(post.id)}
147
+ className="text-left w-full hover:cursor-pointer"
148
+ >
149
+ <h3 className="font-bold text-lg text-neutral-950 dark:text-white mb-2 line-clamp-2 group-hover:text-primary transition-colors hover:underline">
150
+ {post.title}
151
+ </h3>
152
+ </button>
153
+ <p className="text-xs text-neutral-500 dark:text-neutral-400 font-mono">
154
+ /{post.slug}
155
+ </p>
156
+ </div>
157
+
158
+ {/* Excerpt */}
159
+ {post.excerpt && (
160
+ <p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4 line-clamp-2">
161
+ {post.excerpt}
162
+ </p>
163
+ )}
164
+
165
+ {/* Meta Information */}
166
+ <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700">
167
+ {/* Author */}
168
+ <div className="flex items-center gap-2">
169
+ {isPostOwner(post) ? (
170
+ <UserCheck size={14} className="text-primary" />
171
+ ) : (
172
+ <User size={14} className="text-neutral-400" />
173
+ )}
174
+ <span className={`text-xs ${isPostOwner(post) ? 'text-primary font-semibold' : 'text-neutral-600 dark:text-neutral-400'}`}>
175
+ {getAuthorName(post.authorId)}
176
+ {isPostOwner(post) && (
177
+ <span className="ml-1">(You)</span>
178
+ )}
179
+ </span>
180
+ </div>
181
+
182
+ {/* Last Modified */}
183
+ <div className="flex items-center gap-2">
184
+ <Calendar size={14} className="text-neutral-400" />
185
+ <span className="text-xs text-neutral-600 dark:text-neutral-400">
186
+ {formatDate(post.updatedAt, locale)}
187
+ </span>
188
+ </div>
189
+ </div>
190
+
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ );
196
+ }
197
+
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Post Filters Component
3
+ * Search and filter controls for posts
4
+ */
5
+ import { PostStatus } from '../../types/post';
6
+ export interface PostFiltersProps {
7
+ search: string;
8
+ onSearchChange: (value: string) => void;
9
+ statusFilter: PostStatus | 'all';
10
+ onStatusFilterChange: (value: PostStatus | 'all') => void;
11
+ categoryFilter: string;
12
+ onCategoryFilterChange: (value: string) => void;
13
+ categories: string[];
14
+ }
15
+ export declare function PostFilters({ search, onSearchChange, statusFilter, onStatusFilterChange, categoryFilter, onCategoryFilterChange, categories, }: PostFiltersProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=PostFilters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostFilters.d.ts","sourceRoot":"","sources":["PostFilters.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,UAAU,GAAG,KAAK,CAAC;IACjC,oBAAoB,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,KAAK,IAAI,CAAC;IAC1D,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,sBAAsB,EACtB,UAAU,GACb,EAAE,gBAAgB,2CAgElB"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Post Filters Component
3
+ * Search and filter controls for posts
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { Search, Filter, Tag } from 'lucide-react';
10
+ import { PostStatus } from '../../types/post';
11
+
12
+ export interface PostFiltersProps {
13
+ search: string;
14
+ onSearchChange: (value: string) => void;
15
+ statusFilter: PostStatus | 'all';
16
+ onStatusFilterChange: (value: PostStatus | 'all') => void;
17
+ categoryFilter: string;
18
+ onCategoryFilterChange: (value: string) => void;
19
+ categories: string[];
20
+ }
21
+
22
+ export function PostFilters({
23
+ search,
24
+ onSearchChange,
25
+ statusFilter,
26
+ onStatusFilterChange,
27
+ categoryFilter,
28
+ onCategoryFilterChange,
29
+ categories,
30
+ }: PostFiltersProps) {
31
+ return (
32
+ <div className="flex flex-col sm:flex-row gap-4 mb-6">
33
+ {/* Search Input */}
34
+ <div className="relative flex-1">
35
+ <label htmlFor="blog-post-search" className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0" style={{ clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }}>
36
+ Search posts by title or content
37
+ </label>
38
+ <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4" />
39
+ <input
40
+ id="blog-post-search"
41
+ name="blog-post-search"
42
+ type="text"
43
+ value={search}
44
+ onChange={(e) => onSearchChange(e.target.value)}
45
+ placeholder="Search posts by title or content..."
46
+ className="w-full pl-11 pr-4 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all"
47
+ />
48
+ </div>
49
+
50
+ {/* Status Filter */}
51
+ <div className="relative">
52
+ <label htmlFor="blog-post-status-filter" className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0" style={{ clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }}>
53
+ Filter by status
54
+ </label>
55
+ <Filter className="absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" />
56
+ <select
57
+ id="blog-post-status-filter"
58
+ name="blog-post-status-filter"
59
+ value={statusFilter}
60
+ onChange={(e) => onStatusFilterChange(e.target.value as PostStatus | 'all')}
61
+ className="pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]"
62
+ >
63
+ <option value="all">All Statuses</option>
64
+ <option value="published">Published</option>
65
+ <option value="draft">Draft</option>
66
+ <option value="scheduled">Scheduled</option>
67
+ <option value="archived">Archived</option>
68
+ </select>
69
+ </div>
70
+
71
+ {/* Category Filter */}
72
+ <div className="relative">
73
+ <label htmlFor="blog-post-category-filter" className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0" style={{ clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }}>
74
+ Filter by category
75
+ </label>
76
+ <Tag className="absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" />
77
+ <select
78
+ id="blog-post-category-filter"
79
+ name="blog-post-category-filter"
80
+ value={categoryFilter}
81
+ onChange={(e) => onCategoryFilterChange(e.target.value)}
82
+ className="pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]"
83
+ >
84
+ <option value="all">All Categories</option>
85
+ {categories.map((category) => (
86
+ <option key={category} value={category}>
87
+ {category}
88
+ </option>
89
+ ))}
90
+ </select>
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Post Manager View
3
+ * Production-ready listing page for managing blog posts
4
+ * Follows dashboard earth-tone design system
5
+ */
6
+ export interface PostManagerViewProps {
7
+ siteId: string;
8
+ locale: string;
9
+ }
10
+ export declare function PostManagerView({ siteId, locale }: PostManagerViewProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=PostManagerView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PostManagerView.d.ts","sourceRoot":"","sources":["PostManagerView.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,2CAqQvE"}