@jhits/plugin-blog 0.0.1
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/README.md +216 -0
- package/package.json +57 -0
- package/src/api/README.md +224 -0
- package/src/api/categories.ts +43 -0
- package/src/api/check-title.ts +60 -0
- package/src/api/handler.ts +419 -0
- package/src/api/index.ts +33 -0
- package/src/api/route.ts +116 -0
- package/src/api/router.ts +114 -0
- package/src/api-server.ts +11 -0
- package/src/config.ts +161 -0
- package/src/hooks/README.md +91 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useBlog.ts +85 -0
- package/src/hooks/useBlogs.ts +123 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +354 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +141 -0
- package/src/lib/blocks/index.ts +6 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
- package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
- package/src/lib/layouts/blocks/index.ts +8 -0
- package/src/lib/layouts/index.ts +52 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
- package/src/lib/mappers/apiMapper.ts +223 -0
- package/src/lib/migration/index.ts +6 -0
- package/src/lib/migration/mapper.ts +140 -0
- package/src/lib/rich-text/RichTextEditor.tsx +826 -0
- package/src/lib/rich-text/RichTextPreview.tsx +210 -0
- package/src/lib/rich-text/index.ts +10 -0
- package/src/lib/utils/blockHelpers.ts +72 -0
- package/src/lib/utils/configValidation.ts +137 -0
- package/src/lib/utils/index.ts +8 -0
- package/src/lib/utils/slugify.ts +79 -0
- package/src/registry/BlockRegistry.ts +142 -0
- package/src/registry/index.ts +11 -0
- package/src/state/EditorContext.tsx +277 -0
- package/src/state/index.ts +8 -0
- package/src/state/reducer.ts +694 -0
- package/src/state/types.ts +160 -0
- package/src/types/block.ts +269 -0
- package/src/types/index.ts +15 -0
- package/src/types/post.ts +165 -0
- package/src/utils/README.md +75 -0
- package/src/utils/client.ts +122 -0
- package/src/utils/index.ts +9 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
- package/src/views/CanvasEditor/EditorBody.tsx +475 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
- package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
- package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
- package/src/views/CanvasEditor/components/index.ts +17 -0
- package/src/views/CanvasEditor/index.ts +16 -0
- package/src/views/PostManager/EmptyState.tsx +42 -0
- package/src/views/PostManager/PostActionsMenu.tsx +112 -0
- package/src/views/PostManager/PostCards.tsx +192 -0
- package/src/views/PostManager/PostFilters.tsx +80 -0
- package/src/views/PostManager/PostManagerView.tsx +280 -0
- package/src/views/PostManager/PostStats.tsx +81 -0
- package/src/views/PostManager/PostTable.tsx +225 -0
- package/src/views/PostManager/index.ts +15 -0
- package/src/views/Preview/PreviewBridgeView.tsx +64 -0
- package/src/views/Preview/index.ts +7 -0
- package/src/views/README.md +82 -0
- package/src/views/Settings/SettingsView.tsx +298 -0
- package/src/views/Settings/index.ts +7 -0
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
- package/src/views/SlugSEO/index.ts +7 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Blog Configuration
|
|
3
|
+
* Automatically creates required API routes in client apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Automatically creates plugin-blog API catch-all route
|
|
11
|
+
* This route forwards requests to the plugin's API router
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
15
|
+
* This function is kept for backwards compatibility but does nothing
|
|
16
|
+
*/
|
|
17
|
+
export function ensureBlogRoutes() {
|
|
18
|
+
// Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
19
|
+
// No need to generate individual routes anymore
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
// Find the host app directory (where next.config.ts is)
|
|
23
|
+
let appDir = process.cwd();
|
|
24
|
+
const possiblePaths = [
|
|
25
|
+
appDir,
|
|
26
|
+
join(appDir, '..'),
|
|
27
|
+
join(appDir, '..', '..'),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const basePath of possiblePaths) {
|
|
31
|
+
const configPath = join(basePath, 'next.config.ts');
|
|
32
|
+
if (existsSync(configPath)) {
|
|
33
|
+
appDir = basePath;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const apiDir = join(appDir, 'src', 'app', 'api');
|
|
39
|
+
const pluginBlogApiDir = join(apiDir, 'plugin-blog', '[...path]');
|
|
40
|
+
const pluginBlogApiPath = join(pluginBlogApiDir, 'route.ts');
|
|
41
|
+
|
|
42
|
+
// Check if route already exists
|
|
43
|
+
if (existsSync(pluginBlogApiPath)) {
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const existingContent = fs.readFileSync(pluginBlogApiPath, 'utf8');
|
|
46
|
+
if (existingContent.includes('@jhits/plugin-blog') || existingContent.includes('plugin-blog')) {
|
|
47
|
+
// Already set up, skip
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create plugin-blog API catch-all route
|
|
53
|
+
mkdirSync(pluginBlogApiDir, { recursive: true });
|
|
54
|
+
writeFileSync(pluginBlogApiPath, `// Auto-generated by @jhits/plugin-blog - Blog Plugin API
|
|
55
|
+
// This route is automatically created for the blog plugin
|
|
56
|
+
import { NextRequest } from 'next/server';
|
|
57
|
+
import { handleBlogApi } from '@jhits/plugin-blog/api';
|
|
58
|
+
import { MongoClient } from 'mongodb';
|
|
59
|
+
import { cookies } from 'next/headers';
|
|
60
|
+
import jwt from 'jsonwebtoken';
|
|
61
|
+
|
|
62
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
|
63
|
+
const MONGODB_URI = process.env.MONGODB_URI || process.env.MONGO_URI || '';
|
|
64
|
+
|
|
65
|
+
// MongoDB client singleton
|
|
66
|
+
let client: MongoClient | null = null;
|
|
67
|
+
let clientPromise: Promise<MongoClient>;
|
|
68
|
+
|
|
69
|
+
if (!MONGODB_URI) {
|
|
70
|
+
throw new Error('Please add MONGODB_URI to your .env.local file');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.env.NODE_ENV === 'development') {
|
|
74
|
+
const globalWithMongo = global as typeof globalThis & {
|
|
75
|
+
_mongoClientPromise?: Promise<MongoClient>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (!globalWithMongo._mongoClientPromise) {
|
|
79
|
+
client = new MongoClient(MONGODB_URI);
|
|
80
|
+
globalWithMongo._mongoClientPromise = client.connect();
|
|
81
|
+
}
|
|
82
|
+
clientPromise = globalWithMongo._mongoClientPromise;
|
|
83
|
+
} else {
|
|
84
|
+
client = new MongoClient(MONGODB_URI);
|
|
85
|
+
clientPromise = client.connect();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getUserId(req: NextRequest): Promise<string | null> {
|
|
89
|
+
try {
|
|
90
|
+
const cookieStore = await cookies();
|
|
91
|
+
const token = cookieStore.get('auth_token')?.value;
|
|
92
|
+
if (!token) return null;
|
|
93
|
+
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
|
94
|
+
return decoded.id;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Get the API config
|
|
101
|
+
function getBlogApiConfig() {
|
|
102
|
+
return {
|
|
103
|
+
getDb: async () => {
|
|
104
|
+
const client = await clientPromise;
|
|
105
|
+
return { db: () => client.db() };
|
|
106
|
+
},
|
|
107
|
+
getUserId,
|
|
108
|
+
collectionName: 'blogs',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function GET(
|
|
113
|
+
req: NextRequest,
|
|
114
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
115
|
+
) {
|
|
116
|
+
const { path } = await params;
|
|
117
|
+
const config = getBlogApiConfig();
|
|
118
|
+
return handleBlogApi(req, path, config);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function POST(
|
|
122
|
+
req: NextRequest,
|
|
123
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
124
|
+
) {
|
|
125
|
+
const { path } = await params;
|
|
126
|
+
const config = getBlogApiConfig();
|
|
127
|
+
return handleBlogApi(req, path, config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function PUT(
|
|
131
|
+
req: NextRequest,
|
|
132
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
133
|
+
) {
|
|
134
|
+
ensureInitialized();
|
|
135
|
+
const { path } = await params;
|
|
136
|
+
return handleBlogApi(req, path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function DELETE(
|
|
140
|
+
req: NextRequest,
|
|
141
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
142
|
+
) {
|
|
143
|
+
ensureInitialized();
|
|
144
|
+
const { path } = await params;
|
|
145
|
+
return handleBlogApi(req, path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function PATCH(
|
|
149
|
+
req: NextRequest,
|
|
150
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
151
|
+
) {
|
|
152
|
+
ensureInitialized();
|
|
153
|
+
const { path } = await params;
|
|
154
|
+
return handleBlogApi(req, path);
|
|
155
|
+
}
|
|
156
|
+
`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Ignore errors - route might already exist or app structure is different
|
|
159
|
+
console.warn('[plugin-blog] Could not ensure blog routes:', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBlog Hook
|
|
3
|
+
* React hook for fetching a single blog post by slug
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
import { apiToBlogPost, type APIBlogDocument } from '../lib/mappers/apiMapper';
|
|
8
|
+
import { BlogPost } from '../types/post';
|
|
9
|
+
|
|
10
|
+
export interface UseBlogOptions {
|
|
11
|
+
/** Blog post slug */
|
|
12
|
+
slug: string;
|
|
13
|
+
/** API base URL (default: '/api/blogs') */
|
|
14
|
+
apiBaseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseBlogResult {
|
|
18
|
+
/** Blog post data */
|
|
19
|
+
blog: BlogPost | null;
|
|
20
|
+
/** Whether data is currently loading */
|
|
21
|
+
loading: boolean;
|
|
22
|
+
/** Error message if fetch failed */
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Function to refetch the blog post */
|
|
25
|
+
refetch: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* React hook to fetch a single blog post by slug
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { blog, loading, error } = useBlog({ slug: 'my-blog-post' });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function useBlog(options: UseBlogOptions): UseBlogResult {
|
|
37
|
+
const { slug, apiBaseUrl = '/api/plugin-blog' } = options;
|
|
38
|
+
|
|
39
|
+
const [blog, setBlog] = useState<BlogPost | null>(null);
|
|
40
|
+
const [loading, setLoading] = useState(true);
|
|
41
|
+
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const fetchBlog = async () => {
|
|
44
|
+
if (!slug) {
|
|
45
|
+
setLoading(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError(null);
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`${apiBaseUrl}/${slug}`);
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
if (response.status === 404) {
|
|
57
|
+
throw new Error('Blog post not found');
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Failed to fetch blog: ${response.status}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const apiDoc: APIBlogDocument = await response.json();
|
|
63
|
+
const blogPost = apiToBlogPost(apiDoc);
|
|
64
|
+
setBlog(blogPost);
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
console.error('[useBlog] Error fetching blog:', err);
|
|
67
|
+
setError(err.message || 'Failed to fetch blog');
|
|
68
|
+
setBlog(null);
|
|
69
|
+
} finally {
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
fetchBlog();
|
|
76
|
+
}, [slug, apiBaseUrl]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
blog,
|
|
80
|
+
loading,
|
|
81
|
+
error,
|
|
82
|
+
refetch: fetchBlog,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBlogs Hook
|
|
3
|
+
* React hook for fetching blog posts in client applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
import { apiToBlogPost, type APIBlogDocument } from '../lib/mappers/apiMapper';
|
|
8
|
+
import { PostListItem } from '../types/post';
|
|
9
|
+
|
|
10
|
+
export interface UseBlogsOptions {
|
|
11
|
+
/** Maximum number of posts to fetch (default: 10) */
|
|
12
|
+
limit?: number;
|
|
13
|
+
/** Number of posts to skip (default: 0) */
|
|
14
|
+
skip?: number;
|
|
15
|
+
/** Filter by status (published, draft, concept) */
|
|
16
|
+
status?: string;
|
|
17
|
+
/** Whether to fetch all posts for admin (includes drafts) */
|
|
18
|
+
admin?: boolean;
|
|
19
|
+
/** API base URL (default: '/api/blogs') */
|
|
20
|
+
apiBaseUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseBlogsResult {
|
|
24
|
+
/** Array of blog posts */
|
|
25
|
+
blogs: PostListItem[];
|
|
26
|
+
/** Whether data is currently loading */
|
|
27
|
+
loading: boolean;
|
|
28
|
+
/** Error message if fetch failed */
|
|
29
|
+
error: string | null;
|
|
30
|
+
/** Total number of posts available */
|
|
31
|
+
total: number;
|
|
32
|
+
/** Function to refetch blogs */
|
|
33
|
+
refetch: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* React hook to fetch blog posts
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { blogs, loading, error } = useBlogs({ limit: 5 });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
|
|
45
|
+
const {
|
|
46
|
+
limit = 10,
|
|
47
|
+
skip = 0,
|
|
48
|
+
status,
|
|
49
|
+
admin = false,
|
|
50
|
+
apiBaseUrl = '/api/plugin-blog',
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
const [blogs, setBlogs] = useState<PostListItem[]>([]);
|
|
54
|
+
const [loading, setLoading] = useState(true);
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
const [total, setTotal] = useState(0);
|
|
57
|
+
|
|
58
|
+
const fetchBlogs = async () => {
|
|
59
|
+
try {
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
const params = new URLSearchParams();
|
|
64
|
+
if (limit) params.set('limit', limit.toString());
|
|
65
|
+
if (skip) params.set('skip', skip.toString());
|
|
66
|
+
if (status) params.set('status', status);
|
|
67
|
+
if (admin) params.set('admin', 'true');
|
|
68
|
+
|
|
69
|
+
const url = `${apiBaseUrl}?${params.toString()}`;
|
|
70
|
+
const response = await fetch(url);
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Failed to fetch blogs: ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
|
|
78
|
+
// Handle error response
|
|
79
|
+
if (data.error) {
|
|
80
|
+
throw new Error(data.error || 'Failed to fetch blogs');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Convert API format to PostListItem format
|
|
84
|
+
const blogsArray = Array.isArray(data.blogs) ? data.blogs : [];
|
|
85
|
+
const convertedBlogs: PostListItem[] = blogsArray.map((apiDoc: APIBlogDocument) => {
|
|
86
|
+
const blogPost = apiToBlogPost(apiDoc);
|
|
87
|
+
return {
|
|
88
|
+
id: blogPost.id,
|
|
89
|
+
title: blogPost.title,
|
|
90
|
+
slug: blogPost.slug,
|
|
91
|
+
excerpt: blogPost.metadata.excerpt || '',
|
|
92
|
+
status: blogPost.publication.status,
|
|
93
|
+
authorId: blogPost.publication.authorId || '',
|
|
94
|
+
updatedAt: blogPost.updatedAt,
|
|
95
|
+
featuredImage: blogPost.metadata.featuredImage,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
setBlogs(convertedBlogs);
|
|
100
|
+
setTotal(data.total || convertedBlogs.length);
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
console.error('[useBlogs] Error fetching blogs:', err);
|
|
103
|
+
setError(err.message || 'Failed to fetch blogs');
|
|
104
|
+
setBlogs([]);
|
|
105
|
+
setTotal(0);
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
fetchBlogs();
|
|
113
|
+
}, [limit, skip, status, admin, apiBaseUrl]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
blogs,
|
|
117
|
+
loading,
|
|
118
|
+
error,
|
|
119
|
+
total,
|
|
120
|
+
refetch: fetchBlogs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Blog - Server-Only Entry Point
|
|
3
|
+
* This file exports only server-side API handlers
|
|
4
|
+
* Used by the dynamic plugin router via @jhits/plugin-blog/server
|
|
5
|
+
*
|
|
6
|
+
* Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { handleBlogApi as handleApi } from './api/router';
|
|
10
|
+
export { handleBlogApi } from './api/router'; // Keep original export for backward compatibility
|
|
11
|
+
export type { BlogApiRouterConfig } from './api/router';
|
|
12
|
+
|