@jhits/plugin-blog 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +55 -55
- package/src/api/handler.ts +6 -7
- package/src/api/router.ts +1 -4
- package/src/views/PostManager/PostActionsMenu.tsx +1 -1
- package/src/views/PostManager/PostManagerView.tsx +8 -5
- package/src/views/PostManager/PostStats.tsx +5 -5
- package/src/views/PostManager/PostTable.tsx +6 -4
- package/src/api/README.md +0 -224
- package/src/hooks/README.md +0 -91
- package/src/utils/README.md +0 -75
- package/src/views/README.md +0 -82
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@jhits/plugin-blog",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Professional blog management system for the JHITS ecosystem",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "./src/index.tsx",
|
|
9
|
+
"types": "./src/index.tsx",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.tsx",
|
|
13
|
+
"default": "./src/index.tsx"
|
|
7
14
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"typescript": "^5"
|
|
52
|
-
},
|
|
53
|
-
"files": [
|
|
54
|
-
"src",
|
|
55
|
-
"package.json"
|
|
56
|
-
]
|
|
57
|
-
}
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./src/index.server.ts",
|
|
17
|
+
"default": "./src/index.server.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@jhits/plugin-core": "^0.0.1",
|
|
22
|
+
"@jhits/plugin-content": "^0.0.1",
|
|
23
|
+
"@jhits/plugin-images": "^0.0.1",
|
|
24
|
+
"bcrypt": "^6.0.0",
|
|
25
|
+
"framer-motion": "^12.23.26",
|
|
26
|
+
"lucide-react": "^0.562.0",
|
|
27
|
+
"mongodb": "^7.0.0",
|
|
28
|
+
"next-auth": "^4.24.13"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"next": ">=15.0.0",
|
|
32
|
+
"next-intl": ">=4.0.0",
|
|
33
|
+
"next-themes": ">=0.4.0",
|
|
34
|
+
"react": ">=18.0.0",
|
|
35
|
+
"react-dom": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@tailwindcss/postcss": "^4",
|
|
39
|
+
"@types/bcrypt": "^6.0.0",
|
|
40
|
+
"@types/node": "^20.19.27",
|
|
41
|
+
"@types/react": "^19",
|
|
42
|
+
"@types/react-dom": "^19",
|
|
43
|
+
"eslint": "^9",
|
|
44
|
+
"eslint-config-next": "16.1.1",
|
|
45
|
+
"next": "16.1.1",
|
|
46
|
+
"next-intl": "4.6.1",
|
|
47
|
+
"next-themes": "0.4.6",
|
|
48
|
+
"react": "19.2.3",
|
|
49
|
+
"react-dom": "19.2.3",
|
|
50
|
+
"tailwindcss": "^4",
|
|
51
|
+
"typescript": "^5"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"src",
|
|
55
|
+
"package.json"
|
|
56
|
+
]
|
|
57
|
+
}
|
package/src/api/handler.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { NextRequest, NextResponse } from 'next/server';
|
|
11
|
-
import { APIBlogDocument, apiToBlogPost, editorStateToAPI } from '../lib/mappers/apiMapper';
|
|
12
11
|
import { slugify } from '../lib/utils/slugify';
|
|
13
12
|
|
|
14
13
|
export interface BlogApiConfig {
|
|
@@ -68,7 +67,7 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
68
67
|
.find(query)
|
|
69
68
|
.sort({ 'publicationData.date': -1 })
|
|
70
69
|
.skip(skip)
|
|
71
|
-
.limit(limit)
|
|
70
|
+
.limit(isAdminView ? 0 : limit)
|
|
72
71
|
.toArray(),
|
|
73
72
|
blogs.countDocuments(query),
|
|
74
73
|
]);
|
|
@@ -292,7 +291,7 @@ export async function PUT_BY_SLUG(
|
|
|
292
291
|
const hasContent =
|
|
293
292
|
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
294
293
|
(content && Array.isArray(content) && content.length > 0);
|
|
295
|
-
|
|
294
|
+
|
|
296
295
|
// Collect all missing fields for better error messages
|
|
297
296
|
const missingFields: string[] = [];
|
|
298
297
|
if (!summary?.trim()) missingFields.push('summary');
|
|
@@ -303,7 +302,7 @@ export async function PUT_BY_SLUG(
|
|
|
303
302
|
missingFields.push('category');
|
|
304
303
|
}
|
|
305
304
|
if (!hasContent) missingFields.push('content');
|
|
306
|
-
|
|
305
|
+
|
|
307
306
|
if (missingFields.length > 0) {
|
|
308
307
|
console.log('[BlogAPI] PUT_BY_SLUG validation failed:', {
|
|
309
308
|
isPublishing,
|
|
@@ -316,9 +315,9 @@ export async function PUT_BY_SLUG(
|
|
|
316
315
|
contentLength: content?.length || 0,
|
|
317
316
|
});
|
|
318
317
|
return NextResponse.json(
|
|
319
|
-
{
|
|
318
|
+
{
|
|
320
319
|
message: `Missing required fields for publishing: ${missingFields.join(', ')}`,
|
|
321
|
-
missingFields
|
|
320
|
+
missingFields
|
|
322
321
|
},
|
|
323
322
|
{ status: 400 }
|
|
324
323
|
);
|
|
@@ -341,7 +340,7 @@ export async function PUT_BY_SLUG(
|
|
|
341
340
|
} else if (publicationData?.status === 'draft') {
|
|
342
341
|
finalStatus = 'concept';
|
|
343
342
|
}
|
|
344
|
-
|
|
343
|
+
|
|
345
344
|
const updateData = {
|
|
346
345
|
title: title.trim(),
|
|
347
346
|
summary: (summary || '').trim(),
|
package/src/api/router.ts
CHANGED
|
@@ -37,21 +37,18 @@ export async function handleBlogApi(
|
|
|
37
37
|
getUserId: config.getUserId,
|
|
38
38
|
collectionName: config.collectionName || 'blogs',
|
|
39
39
|
});
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
const method = req.method;
|
|
42
42
|
// Handle empty path array - means we're at /api/plugin-blog
|
|
43
43
|
// Ensure path is always an array
|
|
44
44
|
const safePath = Array.isArray(path) ? path : [];
|
|
45
45
|
const route = safePath.length > 0 ? safePath[0] : '';
|
|
46
46
|
|
|
47
|
-
console.log(`[BlogApiRouter] method=${method}, path=${JSON.stringify(safePath)}, route=${route}, url=${req.url}`);
|
|
48
|
-
|
|
49
47
|
try {
|
|
50
48
|
// Route: /api/plugin-blog (list/create) - empty path or 'list'
|
|
51
49
|
// This handles both /api/plugin-blog and /api/plugin-blog?limit=3
|
|
52
50
|
if (!route || route === 'list') {
|
|
53
51
|
if (method === 'GET') {
|
|
54
|
-
console.log('[BlogApiRouter] Routing to BlogListHandler');
|
|
55
52
|
return await BlogListHandler(req, blogApiConfig);
|
|
56
53
|
}
|
|
57
54
|
if (method === 'POST') {
|
|
@@ -69,7 +69,7 @@ export function PostActionsMenu({
|
|
|
69
69
|
const menuContent = isOpen && (
|
|
70
70
|
<div
|
|
71
71
|
ref={menuRef}
|
|
72
|
-
className="fixed w-48 bg-dashboard-card border border-dashboard-border rounded-2xl shadow-xl z-
|
|
72
|
+
className="fixed w-48 bg-dashboard-card border border-dashboard-border rounded-2xl shadow-xl z-9999 overflow-hidden"
|
|
73
73
|
style={{
|
|
74
74
|
top: `${menuPosition.top}px`,
|
|
75
75
|
right: `${menuPosition.right}px`,
|
|
@@ -25,6 +25,7 @@ type ViewMode = 'list' | 'cards';
|
|
|
25
25
|
|
|
26
26
|
export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
27
27
|
const [posts, setPosts] = useState<PostListItem[]>([]);
|
|
28
|
+
const [totalPosts, setTotalPosts] = useState<number>(0);
|
|
28
29
|
const [loading, setLoading] = useState(true);
|
|
29
30
|
const [search, setSearch] = useState('');
|
|
30
31
|
const [statusFilter, setStatusFilter] = useState<PostStatus | 'all'>('all');
|
|
@@ -85,6 +86,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
|
85
86
|
};
|
|
86
87
|
});
|
|
87
88
|
setPosts(postListItems);
|
|
89
|
+
setTotalPosts(data.total);
|
|
88
90
|
}
|
|
89
91
|
} catch (error) {
|
|
90
92
|
console.error('Failed to fetch posts:', error);
|
|
@@ -173,6 +175,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
|
173
175
|
if (response.ok) {
|
|
174
176
|
// Remove from local state
|
|
175
177
|
setPosts((prev) => prev.filter((p) => p.id !== postId));
|
|
178
|
+
setTotalPosts(prev => prev - 1);
|
|
176
179
|
} else {
|
|
177
180
|
const error = await response.json();
|
|
178
181
|
alert(error.error || 'Failed to delete post');
|
|
@@ -210,7 +213,7 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
|
210
213
|
</div>
|
|
211
214
|
|
|
212
215
|
{/* Stats Summary */}
|
|
213
|
-
<PostStats posts={posts} />
|
|
216
|
+
<PostStats total={totalPosts} posts={posts} />
|
|
214
217
|
|
|
215
218
|
{/* Filters & Search Bar with View Toggle */}
|
|
216
219
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
|
@@ -229,8 +232,8 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
|
229
232
|
<button
|
|
230
233
|
onClick={() => setViewMode('list')}
|
|
231
234
|
className={`p-2 rounded-full transition-all ${viewMode === 'list'
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
? 'bg-white dark:bg-neutral-900 text-primary shadow-sm'
|
|
236
|
+
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
|
|
234
237
|
}`}
|
|
235
238
|
title="List View"
|
|
236
239
|
>
|
|
@@ -239,8 +242,8 @@ export function PostManagerView({ siteId, locale }: PostManagerViewProps) {
|
|
|
239
242
|
<button
|
|
240
243
|
onClick={() => setViewMode('cards')}
|
|
241
244
|
className={`p-2 rounded-full transition-all ${viewMode === 'cards'
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
? 'bg-white dark:bg-neutral-900 text-primary shadow-sm'
|
|
246
|
+
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
|
|
244
247
|
}`}
|
|
245
248
|
title="Card View"
|
|
246
249
|
>
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
9
8
|
import { FileText, CheckCircle2, Clock, Archive } from 'lucide-react';
|
|
10
|
-
import { PostListItem
|
|
9
|
+
import { PostListItem } from '../../types/post';
|
|
10
|
+
import { ReactNode } from 'react';
|
|
11
11
|
|
|
12
12
|
export interface PostStatsProps {
|
|
13
13
|
posts: PostListItem[];
|
|
14
|
+
total: Number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function PostStats({ posts }: PostStatsProps) {
|
|
17
|
-
const total = posts.length;
|
|
17
|
+
export function PostStats({ total, posts }: PostStatsProps) {
|
|
18
18
|
const published = posts.filter(p => p.status === 'published').length;
|
|
19
19
|
const drafts = posts.filter(p => p.status === 'draft').length;
|
|
20
20
|
const scheduled = posts.filter(p => p.status === 'scheduled').length;
|
|
@@ -65,7 +65,7 @@ export function PostStats({ posts }: PostStatsProps) {
|
|
|
65
65
|
</div>
|
|
66
66
|
<div>
|
|
67
67
|
<p className="text-2xl font-black text-neutral-950 dark:text-white">
|
|
68
|
-
{stat.value}
|
|
68
|
+
{stat.value as ReactNode}
|
|
69
69
|
</p>
|
|
70
70
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
|
|
71
71
|
{stat.label}
|
|
@@ -56,7 +56,7 @@ export function PostTable({
|
|
|
56
56
|
const { data: session, status: sessionStatus } = useSession();
|
|
57
57
|
const currentUserId = (session?.user as any)?.id;
|
|
58
58
|
const [userMap, setUserMap] = useState<Record<string, string>>({});
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
// Helper function to check if user is the owner
|
|
61
61
|
const isPostOwner = (post: PostListItem): boolean => {
|
|
62
62
|
if (sessionStatus === 'loading') return false; // Don't show actions while loading
|
|
@@ -144,9 +144,11 @@ export function PostTable({
|
|
|
144
144
|
</div>
|
|
145
145
|
)}
|
|
146
146
|
<div className="min-w-0 flex-1">
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
<button onClick={() => onEdit(post.id)} className='hover:cursor-pointer'>
|
|
148
|
+
<h3 className="font-bold hover:underline text-neutral-950 dark:text-white mb-1 line-clamp-1">
|
|
149
|
+
{post.title}
|
|
150
|
+
</h3>
|
|
151
|
+
</button>
|
|
150
152
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 font-mono">
|
|
151
153
|
/{post.slug}
|
|
152
154
|
</p>
|
package/src/api/README.md
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# Blog API
|
|
2
|
-
|
|
3
|
-
RESTful API handlers for blog posts, included in the `@jhits/plugin-blog` package.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
The API handlers are automatically available when you install `@jhits/plugin-blog`.
|
|
8
|
-
|
|
9
|
-
## Setup
|
|
10
|
-
|
|
11
|
-
### 1. Create API Route in Your App
|
|
12
|
-
|
|
13
|
-
Create API routes in your Next.js app to mount the blog API handlers:
|
|
14
|
-
|
|
15
|
-
**`src/app/api/blogs/route.ts`** - List and create blogs:
|
|
16
|
-
```typescript
|
|
17
|
-
import { NextRequest } from 'next/server';
|
|
18
|
-
import { GET, POST, createBlogApiConfig } from '@jhits/plugin-blog/api';
|
|
19
|
-
import clientPromise from '@/lib/mongodb';
|
|
20
|
-
import { cookies } from 'next/headers';
|
|
21
|
-
import jwt from 'jsonwebtoken';
|
|
22
|
-
|
|
23
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
|
24
|
-
|
|
25
|
-
async function getUserId(req: NextRequest): Promise<string | null> {
|
|
26
|
-
try {
|
|
27
|
-
const cookieStore = await cookies();
|
|
28
|
-
const token = cookieStore.get('auth_token')?.value;
|
|
29
|
-
if (!token) return null;
|
|
30
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
|
31
|
-
return decoded.id;
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const config = createBlogApiConfig({
|
|
38
|
-
getDb: async () => clientPromise,
|
|
39
|
-
getUserId,
|
|
40
|
-
collectionName: 'blogs', // optional, defaults to 'blogs'
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export async function GET(req: NextRequest) {
|
|
44
|
-
return GET(req, undefined, config);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function POST(req: NextRequest) {
|
|
48
|
-
return POST(req, undefined, config);
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**`src/app/api/blogs/[slug]/route.ts`** - Get, update, and delete by slug:
|
|
53
|
-
```typescript
|
|
54
|
-
import { NextRequest } from 'next/server';
|
|
55
|
-
import { GET, PUT, DELETE, createBlogApiConfig } from '@jhits/plugin-blog/api';
|
|
56
|
-
import clientPromise from '@/lib/mongodb';
|
|
57
|
-
import { cookies } from 'next/headers';
|
|
58
|
-
import jwt from 'jsonwebtoken';
|
|
59
|
-
|
|
60
|
-
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
|
61
|
-
|
|
62
|
-
async function getUserId(req: NextRequest): Promise<string | null> {
|
|
63
|
-
try {
|
|
64
|
-
const cookieStore = await cookies();
|
|
65
|
-
const token = cookieStore.get('auth_token')?.value;
|
|
66
|
-
if (!token) return null;
|
|
67
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
|
68
|
-
return decoded.id;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const config = createBlogApiConfig({
|
|
75
|
-
getDb: async () => clientPromise,
|
|
76
|
-
getUserId,
|
|
77
|
-
collectionName: 'blogs',
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export async function GET(
|
|
81
|
-
req: NextRequest,
|
|
82
|
-
{ params }: { params: Promise<{ slug: string }> }
|
|
83
|
-
) {
|
|
84
|
-
return GET(req, { params }, config);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function PUT(
|
|
88
|
-
req: NextRequest,
|
|
89
|
-
{ params }: { params: Promise<{ slug: string }> }
|
|
90
|
-
) {
|
|
91
|
-
return PUT(req, { params }, config);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function DELETE(
|
|
95
|
-
req: NextRequest,
|
|
96
|
-
{ params }: { params: Promise<{ slug: string }> }
|
|
97
|
-
) {
|
|
98
|
-
return DELETE(req, { params }, config);
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## API Endpoints
|
|
103
|
-
|
|
104
|
-
### GET /api/blogs
|
|
105
|
-
List all blog posts.
|
|
106
|
-
|
|
107
|
-
**Query Parameters:**
|
|
108
|
-
- `limit` (number, default: 10) - Number of posts to return
|
|
109
|
-
- `skip` (number, default: 0) - Number of posts to skip
|
|
110
|
-
- `status` (string, optional) - Filter by status (`published`, `concept`, `draft`)
|
|
111
|
-
- `admin` (boolean, default: false) - If true, returns all posts for authenticated user (including drafts)
|
|
112
|
-
|
|
113
|
-
**Response:**
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"blogs": [...],
|
|
117
|
-
"total": 100
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### POST /api/blogs
|
|
122
|
-
Create a new blog post.
|
|
123
|
-
|
|
124
|
-
**Request Body:**
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"title": "My Blog Post",
|
|
128
|
-
"summary": "A brief summary",
|
|
129
|
-
"contentBlocks": [...],
|
|
130
|
-
"image": {
|
|
131
|
-
"src": "/uploads/image.jpg",
|
|
132
|
-
"alt": "Image alt text",
|
|
133
|
-
"brightness": 100,
|
|
134
|
-
"blur": 0
|
|
135
|
-
},
|
|
136
|
-
"categoryTags": {
|
|
137
|
-
"category": "Technology",
|
|
138
|
-
"tags": ["web", "development"]
|
|
139
|
-
},
|
|
140
|
-
"publicationData": {
|
|
141
|
-
"status": "concept" | "published",
|
|
142
|
-
"date": "2024-01-01T00:00:00.000Z"
|
|
143
|
-
},
|
|
144
|
-
"seo": {
|
|
145
|
-
"title": "SEO Title",
|
|
146
|
-
"description": "SEO Description",
|
|
147
|
-
"keywords": ["keyword1", "keyword2"],
|
|
148
|
-
"ogImage": "/uploads/og-image.jpg",
|
|
149
|
-
"canonicalUrl": "https://example.com/blog/my-post"
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Response:**
|
|
155
|
-
```json
|
|
156
|
-
{
|
|
157
|
-
"message": "Draft saved successfully",
|
|
158
|
-
"blogId": "...",
|
|
159
|
-
"slug": "my-blog-post-draft-1234"
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### GET /api/blogs/[slug]
|
|
164
|
-
Get a single blog post by slug.
|
|
165
|
-
|
|
166
|
-
**Response:**
|
|
167
|
-
```json
|
|
168
|
-
{
|
|
169
|
-
"_id": "...",
|
|
170
|
-
"title": "My Blog Post",
|
|
171
|
-
"slug": "my-blog-post",
|
|
172
|
-
"contentBlocks": [...],
|
|
173
|
-
...
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### PUT /api/blogs/[slug]
|
|
178
|
-
Update a blog post by slug.
|
|
179
|
-
|
|
180
|
-
**Request Body:** Same as POST /api/blogs
|
|
181
|
-
|
|
182
|
-
**Response:**
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"message": "Blog updated successfully",
|
|
186
|
-
"slug": "updated-slug"
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### DELETE /api/blogs/[slug]
|
|
191
|
-
Delete a blog post by slug.
|
|
192
|
-
|
|
193
|
-
**Response:**
|
|
194
|
-
```json
|
|
195
|
-
{
|
|
196
|
-
"message": "Blog deleted successfully"
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Security
|
|
201
|
-
|
|
202
|
-
- All endpoints (except public GET) require authentication
|
|
203
|
-
- Users can only access/modify their own posts
|
|
204
|
-
- Published posts are visible to everyone
|
|
205
|
-
- Drafts/concepts are only visible to the author
|
|
206
|
-
|
|
207
|
-
## Error Responses
|
|
208
|
-
|
|
209
|
-
All endpoints return standard HTTP status codes:
|
|
210
|
-
- `200` - Success
|
|
211
|
-
- `400` - Bad Request (validation errors)
|
|
212
|
-
- `401` - Unauthorized (not authenticated)
|
|
213
|
-
- `403` - Forbidden (not authorized)
|
|
214
|
-
- `404` - Not Found
|
|
215
|
-
- `500` - Internal Server Error
|
|
216
|
-
|
|
217
|
-
Error response format:
|
|
218
|
-
```json
|
|
219
|
-
{
|
|
220
|
-
"error": "Error message",
|
|
221
|
-
"detail": "Detailed error information"
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
package/src/hooks/README.md
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Blog Plugin Hooks
|
|
2
|
-
|
|
3
|
-
React hooks for fetching blog data in client applications.
|
|
4
|
-
|
|
5
|
-
## useBlogs
|
|
6
|
-
|
|
7
|
-
Fetch a list of blog posts with automatic loading and error handling.
|
|
8
|
-
|
|
9
|
-
### Usage
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
import { useBlogs } from '@jhits/plugin-blog';
|
|
13
|
-
|
|
14
|
-
function MyComponent() {
|
|
15
|
-
const { blogs, loading, error, total, refetch } = useBlogs({
|
|
16
|
-
limit: 10,
|
|
17
|
-
skip: 0,
|
|
18
|
-
status: 'published',
|
|
19
|
-
admin: false,
|
|
20
|
-
apiBaseUrl: '/api/blogs',
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (loading) return <div>Loading...</div>;
|
|
24
|
-
if (error) return <div>Error: {error}</div>;
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div>
|
|
28
|
-
{blogs.map(blog => (
|
|
29
|
-
<div key={blog.id}>
|
|
30
|
-
<h2>{blog.title}</h2>
|
|
31
|
-
<p>{blog.excerpt}</p>
|
|
32
|
-
</div>
|
|
33
|
-
))}
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Options
|
|
40
|
-
|
|
41
|
-
- `limit` (number, default: 10): Maximum number of posts to fetch
|
|
42
|
-
- `skip` (number, default: 0): Number of posts to skip (for pagination)
|
|
43
|
-
- `status` (string, optional): Filter by status ('published', 'draft', 'concept')
|
|
44
|
-
- `admin` (boolean, default: false): Whether to fetch all posts for admin (includes drafts)
|
|
45
|
-
- `apiBaseUrl` (string, default: '/api/blogs'): API base URL
|
|
46
|
-
|
|
47
|
-
### Returns
|
|
48
|
-
|
|
49
|
-
- `blogs` (PostListItem[]): Array of blog posts
|
|
50
|
-
- `loading` (boolean): Whether data is currently loading
|
|
51
|
-
- `error` (string | null): Error message if fetch failed
|
|
52
|
-
- `total` (number): Total number of posts available
|
|
53
|
-
- `refetch` (function): Function to manually refetch blogs
|
|
54
|
-
|
|
55
|
-
## useBlog
|
|
56
|
-
|
|
57
|
-
Fetch a single blog post by slug.
|
|
58
|
-
|
|
59
|
-
### Usage
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
import { useBlog } from '@jhits/plugin-blog';
|
|
63
|
-
|
|
64
|
-
function BlogPost({ slug }: { slug: string }) {
|
|
65
|
-
const { blog, loading, error, refetch } = useBlog({ slug });
|
|
66
|
-
|
|
67
|
-
if (loading) return <div>Loading...</div>;
|
|
68
|
-
if (error) return <div>Error: {error}</div>;
|
|
69
|
-
if (!blog) return <div>Post not found</div>;
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<article>
|
|
73
|
-
<h1>{blog.title}</h1>
|
|
74
|
-
<div>{/* Render blog content */}</div>
|
|
75
|
-
</article>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Options
|
|
81
|
-
|
|
82
|
-
- `slug` (string, required): Blog post slug
|
|
83
|
-
- `apiBaseUrl` (string, default: '/api/blogs'): API base URL
|
|
84
|
-
|
|
85
|
-
### Returns
|
|
86
|
-
|
|
87
|
-
- `blog` (BlogPost | null): Blog post data
|
|
88
|
-
- `loading` (boolean): Whether data is currently loading
|
|
89
|
-
- `error` (string | null): Error message if fetch failed
|
|
90
|
-
- `refetch` (function): Function to manually refetch the blog post
|
|
91
|
-
|
package/src/utils/README.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Blog Plugin Client Utilities
|
|
2
|
-
|
|
3
|
-
Helper functions for fetching blog data in client applications (non-React).
|
|
4
|
-
|
|
5
|
-
## fetchBlogs
|
|
6
|
-
|
|
7
|
-
Fetch blog posts from the API (returns a Promise).
|
|
8
|
-
|
|
9
|
-
### Usage
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { fetchBlogs } from '@jhits/plugin-blog';
|
|
13
|
-
|
|
14
|
-
async function loadBlogs() {
|
|
15
|
-
try {
|
|
16
|
-
const { blogs, total } = await fetchBlogs({
|
|
17
|
-
limit: 10,
|
|
18
|
-
skip: 0,
|
|
19
|
-
status: 'published',
|
|
20
|
-
admin: false,
|
|
21
|
-
apiBaseUrl: '/api/blogs',
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
console.log(`Found ${total} posts`);
|
|
25
|
-
blogs.forEach(blog => {
|
|
26
|
-
console.log(blog.title);
|
|
27
|
-
});
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error('Failed to fetch blogs:', error);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Options
|
|
35
|
-
|
|
36
|
-
- `limit` (number, default: 10): Maximum number of posts to fetch
|
|
37
|
-
- `skip` (number, default: 0): Number of posts to skip (for pagination)
|
|
38
|
-
- `status` (string, optional): Filter by status ('published', 'draft', 'concept')
|
|
39
|
-
- `admin` (boolean, default: false): Whether to fetch all posts for admin (includes drafts)
|
|
40
|
-
- `apiBaseUrl` (string, default: '/api/blogs'): API base URL
|
|
41
|
-
|
|
42
|
-
### Returns
|
|
43
|
-
|
|
44
|
-
Promise resolving to:
|
|
45
|
-
- `blogs` (PostListItem[]): Array of blog posts
|
|
46
|
-
- `total` (number): Total number of posts available
|
|
47
|
-
|
|
48
|
-
## fetchBlog
|
|
49
|
-
|
|
50
|
-
Fetch a single blog post by slug (returns a Promise).
|
|
51
|
-
|
|
52
|
-
### Usage
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
import { fetchBlog } from '@jhits/plugin-blog';
|
|
56
|
-
|
|
57
|
-
async function loadBlogPost(slug: string) {
|
|
58
|
-
try {
|
|
59
|
-
const blog = await fetchBlog({ slug });
|
|
60
|
-
console.log(blog.title);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Failed to fetch blog:', error);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Options
|
|
68
|
-
|
|
69
|
-
- `slug` (string, required): Blog post slug
|
|
70
|
-
- `apiBaseUrl` (string, default: '/api/blogs'): API base URL
|
|
71
|
-
|
|
72
|
-
### Returns
|
|
73
|
-
|
|
74
|
-
Promise resolving to a `BlogPost` object.
|
|
75
|
-
|
package/src/views/README.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# Blog Plugin Views
|
|
2
|
-
|
|
3
|
-
## Directory Structure
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
src/views/
|
|
7
|
-
├── PostManager/
|
|
8
|
-
│ ├── PostManagerView.tsx # Professional table layout for post management
|
|
9
|
-
│ └── index.ts
|
|
10
|
-
├── CanvasEditor/
|
|
11
|
-
│ ├── CanvasEditorView.tsx # Drag-and-drop block editor workspace
|
|
12
|
-
│ └── index.ts
|
|
13
|
-
├── SlugSEO/
|
|
14
|
-
│ ├── SlugSEOManagerView.tsx # URL slug and SEO metadata management
|
|
15
|
-
│ └── index.ts
|
|
16
|
-
└── Preview/
|
|
17
|
-
├── PreviewBridgeView.tsx # Live preview of blog posts
|
|
18
|
-
└── index.ts
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Design System Compliance
|
|
22
|
-
|
|
23
|
-
All views follow the dashboard's earth-tone design system:
|
|
24
|
-
|
|
25
|
-
- **Colors**: Neutral palette (neutral-100 to neutral-900), primary (violet-700)
|
|
26
|
-
- **Typography**:
|
|
27
|
-
- Headings: `font-black uppercase tracking-tighter`
|
|
28
|
-
- Labels: `text-[10px] font-black uppercase tracking-widest`
|
|
29
|
-
- Body: `font-sans`
|
|
30
|
-
- **Layout**:
|
|
31
|
-
- Containers: `rounded-[2.5rem]`
|
|
32
|
-
- Padding: `p-8`
|
|
33
|
-
- Backgrounds: `bg-white dark:bg-neutral-900`
|
|
34
|
-
- **Buttons**: `rounded-full`, uppercase, tracking-widest
|
|
35
|
-
- **Borders**: `border-neutral-300 dark:border-neutral-700`
|
|
36
|
-
|
|
37
|
-
## PostManagerView
|
|
38
|
-
|
|
39
|
-
Professional table layout with:
|
|
40
|
-
- Search and filter functionality
|
|
41
|
-
- Status badges (published, draft, scheduled, archived)
|
|
42
|
-
- Empty state with "Create Your First Post" CTA
|
|
43
|
-
- Action buttons (Edit, Preview, Delete)
|
|
44
|
-
- Mock data for development
|
|
45
|
-
|
|
46
|
-
## CanvasEditorView
|
|
47
|
-
|
|
48
|
-
Block editor workspace with:
|
|
49
|
-
- Toolbar with Back, Preview, and Save buttons
|
|
50
|
-
- Title input field
|
|
51
|
-
- Block canvas area
|
|
52
|
-
- Empty state for adding first block
|
|
53
|
-
- Integrated with EditorProvider for state management
|
|
54
|
-
|
|
55
|
-
## SlugSEOManagerView
|
|
56
|
-
|
|
57
|
-
SEO management interface with:
|
|
58
|
-
- URL slug editor with collision detection placeholder
|
|
59
|
-
- Google-style search result preview
|
|
60
|
-
- Meta description editor with character counter
|
|
61
|
-
|
|
62
|
-
## PreviewBridgeView
|
|
63
|
-
|
|
64
|
-
Live preview interface with:
|
|
65
|
-
- Header with Refresh and Open in New Tab buttons
|
|
66
|
-
- Preview container (ready for iframe or side-by-side panel)
|
|
67
|
-
- Placeholder for preview rendering
|
|
68
|
-
|
|
69
|
-
## Usage
|
|
70
|
-
|
|
71
|
-
Views are imported and used in the main `BlogPlugin` router:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
import { PostManagerView } from './views/PostManager';
|
|
75
|
-
import { CanvasEditorView } from './views/CanvasEditor';
|
|
76
|
-
// ... etc
|
|
77
|
-
|
|
78
|
-
// In router switch statement:
|
|
79
|
-
case 'posts':
|
|
80
|
-
return <PostManagerView siteId={siteId} locale={locale} />;
|
|
81
|
-
```
|
|
82
|
-
|