@123resume/react-blog-system 1.0.0
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 +324 -0
- package/dist/index.d.mts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +241 -0
- package/dist/index.mjs +213 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# @123resume/react-blog-system
|
|
2
|
+
|
|
3
|
+
A React blog system with multi-language support, content management, and scheduled publishing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Multi-language support for creating and managing posts in different languages
|
|
8
|
+
- Markdown-based editor with live preview
|
|
9
|
+
- Scheduled publishing with date and time support
|
|
10
|
+
- Draft management and publishing workflow
|
|
11
|
+
- Customizable styling with gradient backgrounds and cover images
|
|
12
|
+
- Image watermarking support
|
|
13
|
+
- Authentication integration
|
|
14
|
+
- Responsive design
|
|
15
|
+
- TypeScript support
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @123resume/react-blog-system
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Configure the API Client
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createBlogClient } from "@123resume/react-blog-system";
|
|
29
|
+
|
|
30
|
+
const blogClient = createBlogClient({
|
|
31
|
+
apiBaseUrl: "https://api.example.com/api",
|
|
32
|
+
getAuthToken: () => localStorage.getItem("authToken"),
|
|
33
|
+
defaultLanguage: "en",
|
|
34
|
+
supportedLanguages: ["en", "de"],
|
|
35
|
+
watermark: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
text: "My Blog",
|
|
38
|
+
icon: FileText,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Basic Usage
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { Blog, BlogPost } from "@123resume/react-blog-system";
|
|
47
|
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
48
|
+
|
|
49
|
+
function App() {
|
|
50
|
+
return (
|
|
51
|
+
<BrowserRouter>
|
|
52
|
+
<Routes>
|
|
53
|
+
<Route path="/blog" element={<Blog client={blogClient} />} />
|
|
54
|
+
<Route path="/blog/:id" element={<BlogPost client={blogClient} />} />
|
|
55
|
+
</Routes>
|
|
56
|
+
</BrowserRouter>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Multi-Language Support
|
|
62
|
+
|
|
63
|
+
The library supports creating separate versions of the same post in different languages. Each language version uses the same post ID but different language codes.
|
|
64
|
+
|
|
65
|
+
### Creating Posts in Multiple Languages
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// English version
|
|
69
|
+
await blogClient.create({
|
|
70
|
+
id: "my-post",
|
|
71
|
+
title: "My Blog Post",
|
|
72
|
+
excerpt: "This is a great post",
|
|
73
|
+
content: "...",
|
|
74
|
+
language: "en",
|
|
75
|
+
// ... other fields
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// German version (same ID, different language)
|
|
79
|
+
await blogClient.create({
|
|
80
|
+
id: "my-post",
|
|
81
|
+
title: "Mein Blog-Post",
|
|
82
|
+
excerpt: "Das ist ein großartiger Post",
|
|
83
|
+
content: "...",
|
|
84
|
+
language: "de",
|
|
85
|
+
// ... other fields
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Fetching Posts by Language
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Get English posts
|
|
93
|
+
const englishPosts = await blogClient.getAll("en");
|
|
94
|
+
|
|
95
|
+
// Get German posts
|
|
96
|
+
const germanPosts = await blogClient.getAll("de");
|
|
97
|
+
|
|
98
|
+
// Get all posts including drafts (for authenticated users)
|
|
99
|
+
const allPosts = await blogClient.getAll("en", true);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Language-Aware Components
|
|
103
|
+
|
|
104
|
+
Components automatically handle language detection based on your application's language context:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useLanguage } from "@123resume/react-blog-system";
|
|
108
|
+
|
|
109
|
+
function BlogPage() {
|
|
110
|
+
const { language } = useLanguage();
|
|
111
|
+
const posts = await blogClient.getAll(language);
|
|
112
|
+
|
|
113
|
+
return <BlogList posts={posts} />;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Blog Editor
|
|
118
|
+
|
|
119
|
+
### Basic Usage
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { BlogEditor } from "@123resume/react-blog-system";
|
|
123
|
+
|
|
124
|
+
function CreatePost() {
|
|
125
|
+
return (
|
|
126
|
+
<BlogEditor
|
|
127
|
+
client={blogClient}
|
|
128
|
+
onSave={(post) => console.log("Saved:", post)}
|
|
129
|
+
onPublish={(post) => console.log("Published:", post)}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Editing Existing Posts
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<BlogEditor client={blogClient} postId="existing-post-id" language="en" />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Editor Features
|
|
142
|
+
|
|
143
|
+
- Auto-generated URL slugs from post titles
|
|
144
|
+
- Markdown editor with live preview
|
|
145
|
+
- Image upload with watermark support
|
|
146
|
+
- Scheduled publishing with date and time picker
|
|
147
|
+
- Draft management
|
|
148
|
+
- Language selection
|
|
149
|
+
- Category and metadata management
|
|
150
|
+
|
|
151
|
+
## Configuration
|
|
152
|
+
|
|
153
|
+
### Watermark
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const blogClient = createBlogClient({
|
|
157
|
+
apiBaseUrl: "https://api.example.com/api",
|
|
158
|
+
watermark: {
|
|
159
|
+
enabled: true,
|
|
160
|
+
text: "My Brand",
|
|
161
|
+
icon: CustomIcon,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Routes
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const blogClient = createBlogClient({
|
|
170
|
+
apiBaseUrl: "https://api.example.com/api",
|
|
171
|
+
routes: {
|
|
172
|
+
blog: "/blog",
|
|
173
|
+
editor: "/blog-editor",
|
|
174
|
+
management: "/blog-management",
|
|
175
|
+
post: (id) => `/blog/${id}`,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Styling
|
|
181
|
+
|
|
182
|
+
The library uses Tailwind CSS classes. Customize by:
|
|
183
|
+
|
|
184
|
+
1. Overriding CSS classes in your application
|
|
185
|
+
2. Using CSS variables for theming
|
|
186
|
+
3. Wrapping components with custom styled containers
|
|
187
|
+
|
|
188
|
+
## Backend API Requirements
|
|
189
|
+
|
|
190
|
+
Your backend must implement the blog API contract. See [BACKEND_API.md](./BACKEND_API.md) for endpoint specifications.
|
|
191
|
+
|
|
192
|
+
### Required Endpoints
|
|
193
|
+
|
|
194
|
+
- `GET /api/blog-posts/` - List posts
|
|
195
|
+
- `GET /api/blog-posts/:id/` - Get post by ID
|
|
196
|
+
- `POST /api/blog-posts/` - Create post (authenticated)
|
|
197
|
+
- `PUT /api/blog-posts/:id/` - Update post (authenticated)
|
|
198
|
+
- `DELETE /api/blog-posts/:id/` - Delete post (authenticated)
|
|
199
|
+
|
|
200
|
+
All endpoints support a `language` query parameter for multi-language support.
|
|
201
|
+
|
|
202
|
+
## API Reference
|
|
203
|
+
|
|
204
|
+
### `createBlogClient(config: BlogConfig): BlogClient`
|
|
205
|
+
|
|
206
|
+
Creates a configured blog API client.
|
|
207
|
+
|
|
208
|
+
**Parameters:**
|
|
209
|
+
|
|
210
|
+
- `config.apiBaseUrl` (string, required) - Base URL for your API
|
|
211
|
+
- `config.getAuthToken` (function, optional) - Function that returns auth token
|
|
212
|
+
- `config.onError` (function, optional) - Error handler callback
|
|
213
|
+
- `config.watermark` (object, optional) - Watermark configuration
|
|
214
|
+
- `config.routes` (object, optional) - Route configuration
|
|
215
|
+
- `config.supportedLanguages` (string[], optional) - Supported language codes
|
|
216
|
+
- `config.defaultLanguage` (string, optional) - Default language code
|
|
217
|
+
|
|
218
|
+
### `BlogClient`
|
|
219
|
+
|
|
220
|
+
#### `getAll(language?: string, includeDrafts?: boolean): Promise<BlogPost[]>`
|
|
221
|
+
|
|
222
|
+
Get all blog posts for a language.
|
|
223
|
+
|
|
224
|
+
#### `getById(id: string, language?: string): Promise<BlogPost>`
|
|
225
|
+
|
|
226
|
+
Get a specific blog post.
|
|
227
|
+
|
|
228
|
+
#### `create(post: BlogPost): Promise<BlogPost>`
|
|
229
|
+
|
|
230
|
+
Create a new blog post.
|
|
231
|
+
|
|
232
|
+
#### `update(id: string, post: Partial<BlogPost>, language?: string): Promise<BlogPost>`
|
|
233
|
+
|
|
234
|
+
Update an existing blog post.
|
|
235
|
+
|
|
236
|
+
#### `delete(id: string, language?: string): Promise<void>`
|
|
237
|
+
|
|
238
|
+
Delete a blog post.
|
|
239
|
+
|
|
240
|
+
## Use Cases
|
|
241
|
+
|
|
242
|
+
### Multi-Language Blog
|
|
243
|
+
|
|
244
|
+
Create separate versions of your blog in English and German:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// English version
|
|
248
|
+
await blogClient.create({
|
|
249
|
+
id: "welcome",
|
|
250
|
+
title: "Welcome to Our Blog",
|
|
251
|
+
language: "en",
|
|
252
|
+
// ...
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// German version
|
|
256
|
+
await blogClient.create({
|
|
257
|
+
id: "welcome",
|
|
258
|
+
title: "Willkommen in unserem Blog",
|
|
259
|
+
language: "de",
|
|
260
|
+
// ...
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Scheduled Publishing
|
|
265
|
+
|
|
266
|
+
Schedule posts to publish automatically:
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
await blogClient.create({
|
|
270
|
+
id: "announcement",
|
|
271
|
+
title: "Big Announcement",
|
|
272
|
+
published: false,
|
|
273
|
+
scheduledPublishAt: "2025-12-25T10:00:00Z",
|
|
274
|
+
// ...
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Draft Workflow
|
|
279
|
+
|
|
280
|
+
Save drafts and publish when ready:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Save as draft
|
|
284
|
+
await blogClient.create({
|
|
285
|
+
id: "draft-post",
|
|
286
|
+
title: "Work in Progress",
|
|
287
|
+
published: false,
|
|
288
|
+
// ...
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Publish later
|
|
292
|
+
await blogClient.update("draft-post", {
|
|
293
|
+
published: true,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Development
|
|
298
|
+
|
|
299
|
+
### Building
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
npm run build
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Development Mode
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npm run dev
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Examples
|
|
312
|
+
|
|
313
|
+
See the [examples](./examples/) directory for complete usage examples:
|
|
314
|
+
|
|
315
|
+
- [basic-usage.tsx](./examples/basic-usage.tsx) - Basic setup and usage
|
|
316
|
+
- [multi-language.tsx](./examples/multi-language.tsx) - Multi-language examples
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|
|
321
|
+
|
|
322
|
+
## Support
|
|
323
|
+
|
|
324
|
+
For issues and questions, open an issue on GitHub.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog System Types
|
|
3
|
+
*/
|
|
4
|
+
interface BlogPost {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
excerpt: string;
|
|
8
|
+
category: string;
|
|
9
|
+
readTime: string;
|
|
10
|
+
date: string;
|
|
11
|
+
gradient: string;
|
|
12
|
+
iconColor: string;
|
|
13
|
+
image?: string;
|
|
14
|
+
content: string;
|
|
15
|
+
language?: string;
|
|
16
|
+
published?: boolean;
|
|
17
|
+
scheduledPublishAt?: string;
|
|
18
|
+
authorId?: string;
|
|
19
|
+
createdAt?: string;
|
|
20
|
+
updatedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
interface BlogConfig {
|
|
23
|
+
apiBaseUrl: string;
|
|
24
|
+
getAuthToken?: () => string | null;
|
|
25
|
+
onError?: (error: Error) => void;
|
|
26
|
+
watermark?: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
text?: string;
|
|
29
|
+
icon?: React.ComponentType<{
|
|
30
|
+
className?: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
routes?: {
|
|
34
|
+
blog?: string;
|
|
35
|
+
editor?: string;
|
|
36
|
+
management?: string;
|
|
37
|
+
post?: (id: string) => string;
|
|
38
|
+
};
|
|
39
|
+
supportedLanguages?: string[];
|
|
40
|
+
defaultLanguage?: string;
|
|
41
|
+
}
|
|
42
|
+
interface BlogClient {
|
|
43
|
+
getAll: (language?: string, includeDrafts?: boolean) => Promise<BlogPost[]>;
|
|
44
|
+
getById: (id: string, language?: string) => Promise<BlogPost>;
|
|
45
|
+
create: (post: BlogPost) => Promise<BlogPost>;
|
|
46
|
+
update: (id: string, post: Partial<BlogPost>, language?: string) => Promise<BlogPost>;
|
|
47
|
+
delete: (id: string, language?: string) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Blog API Client
|
|
52
|
+
* Configurable client for blog post operations
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a blog client with the given configuration
|
|
57
|
+
*/
|
|
58
|
+
declare const createBlogClient: (config: BlogConfig) => BlogClient;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Simple Markdown Renderer
|
|
62
|
+
* Converts markdown-like text to React elements
|
|
63
|
+
*/
|
|
64
|
+
declare const renderMarkdown: (content: string) => JSX.Element[];
|
|
65
|
+
|
|
66
|
+
export { type BlogClient, type BlogConfig, type BlogPost, createBlogClient, renderMarkdown };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog System Types
|
|
3
|
+
*/
|
|
4
|
+
interface BlogPost {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
excerpt: string;
|
|
8
|
+
category: string;
|
|
9
|
+
readTime: string;
|
|
10
|
+
date: string;
|
|
11
|
+
gradient: string;
|
|
12
|
+
iconColor: string;
|
|
13
|
+
image?: string;
|
|
14
|
+
content: string;
|
|
15
|
+
language?: string;
|
|
16
|
+
published?: boolean;
|
|
17
|
+
scheduledPublishAt?: string;
|
|
18
|
+
authorId?: string;
|
|
19
|
+
createdAt?: string;
|
|
20
|
+
updatedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
interface BlogConfig {
|
|
23
|
+
apiBaseUrl: string;
|
|
24
|
+
getAuthToken?: () => string | null;
|
|
25
|
+
onError?: (error: Error) => void;
|
|
26
|
+
watermark?: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
text?: string;
|
|
29
|
+
icon?: React.ComponentType<{
|
|
30
|
+
className?: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
routes?: {
|
|
34
|
+
blog?: string;
|
|
35
|
+
editor?: string;
|
|
36
|
+
management?: string;
|
|
37
|
+
post?: (id: string) => string;
|
|
38
|
+
};
|
|
39
|
+
supportedLanguages?: string[];
|
|
40
|
+
defaultLanguage?: string;
|
|
41
|
+
}
|
|
42
|
+
interface BlogClient {
|
|
43
|
+
getAll: (language?: string, includeDrafts?: boolean) => Promise<BlogPost[]>;
|
|
44
|
+
getById: (id: string, language?: string) => Promise<BlogPost>;
|
|
45
|
+
create: (post: BlogPost) => Promise<BlogPost>;
|
|
46
|
+
update: (id: string, post: Partial<BlogPost>, language?: string) => Promise<BlogPost>;
|
|
47
|
+
delete: (id: string, language?: string) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Blog API Client
|
|
52
|
+
* Configurable client for blog post operations
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a blog client with the given configuration
|
|
57
|
+
*/
|
|
58
|
+
declare const createBlogClient: (config: BlogConfig) => BlogClient;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Simple Markdown Renderer
|
|
62
|
+
* Converts markdown-like text to React elements
|
|
63
|
+
*/
|
|
64
|
+
declare const renderMarkdown: (content: string) => JSX.Element[];
|
|
65
|
+
|
|
66
|
+
export { type BlogClient, type BlogConfig, type BlogPost, createBlogClient, renderMarkdown };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createBlogClient: () => createBlogClient,
|
|
24
|
+
renderMarkdown: () => renderMarkdown
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/api/blogClient.ts
|
|
29
|
+
var camelToSnake = (str) => {
|
|
30
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
31
|
+
};
|
|
32
|
+
var camelToSnakeObject = (obj) => {
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
return obj.map(camelToSnakeObject);
|
|
35
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
36
|
+
const newObj = {};
|
|
37
|
+
for (const key in obj) {
|
|
38
|
+
if (obj.hasOwnProperty(key)) {
|
|
39
|
+
const snakeKey = camelToSnake(key);
|
|
40
|
+
newObj[snakeKey] = camelToSnakeObject(obj[key]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return newObj;
|
|
44
|
+
}
|
|
45
|
+
return obj;
|
|
46
|
+
};
|
|
47
|
+
var snakeToCamel = (str) => {
|
|
48
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
49
|
+
};
|
|
50
|
+
var snakeToCamelObject = (obj) => {
|
|
51
|
+
if (Array.isArray(obj)) {
|
|
52
|
+
return obj.map(snakeToCamelObject);
|
|
53
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
54
|
+
const newObj = {};
|
|
55
|
+
for (const key in obj) {
|
|
56
|
+
if (obj.hasOwnProperty(key)) {
|
|
57
|
+
const camelKey = snakeToCamel(key);
|
|
58
|
+
newObj[camelKey] = snakeToCamelObject(obj[key]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return newObj;
|
|
62
|
+
}
|
|
63
|
+
return obj;
|
|
64
|
+
};
|
|
65
|
+
var createBlogClient = (config) => {
|
|
66
|
+
const createHeaders = (requireAuth = false) => {
|
|
67
|
+
const headers = {
|
|
68
|
+
"Content-Type": "application/json"
|
|
69
|
+
};
|
|
70
|
+
if (requireAuth && config.getAuthToken) {
|
|
71
|
+
const token = config.getAuthToken();
|
|
72
|
+
if (token) {
|
|
73
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return headers;
|
|
77
|
+
};
|
|
78
|
+
const handleError = (error) => {
|
|
79
|
+
if (config.onError) {
|
|
80
|
+
config.onError(error);
|
|
81
|
+
} else {
|
|
82
|
+
console.error("Blog API Error:", error);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
/**
|
|
88
|
+
* Get all published blog posts for a language
|
|
89
|
+
*/
|
|
90
|
+
getAll: async (language = config.defaultLanguage || "en", includeDrafts = false) => {
|
|
91
|
+
try {
|
|
92
|
+
const url = `${config.apiBaseUrl}/blog-posts/?language=${language}${includeDrafts ? "&include_drafts=true" : ""}`;
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: createHeaders(includeDrafts)
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const error = await response.json().catch(() => ({}));
|
|
99
|
+
throw new Error(error.error || "Failed to fetch blog posts");
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
return snakeToCamelObject(data);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return handleError(error);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Get a specific blog post by ID
|
|
109
|
+
*/
|
|
110
|
+
getById: async (id, language = config.defaultLanguage || "en") => {
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
113
|
+
method: "GET",
|
|
114
|
+
headers: createHeaders(false)
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.json().catch(() => ({}));
|
|
118
|
+
throw new Error(error.error || "Failed to fetch blog post");
|
|
119
|
+
}
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
return snakeToCamelObject(data);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return handleError(error);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* Create a new blog post
|
|
128
|
+
*/
|
|
129
|
+
create: async (post) => {
|
|
130
|
+
try {
|
|
131
|
+
const snakeCaseData = camelToSnakeObject(post);
|
|
132
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: createHeaders(true),
|
|
135
|
+
body: JSON.stringify(snakeCaseData)
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const error = await response.json().catch(() => ({}));
|
|
139
|
+
throw new Error(error.error || error.message || "Failed to create blog post");
|
|
140
|
+
}
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
return snakeToCamelObject(data);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return handleError(error);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* Update an existing blog post
|
|
149
|
+
*/
|
|
150
|
+
update: async (id, post, language = config.defaultLanguage || "en") => {
|
|
151
|
+
try {
|
|
152
|
+
const snakeCaseData = camelToSnakeObject(post);
|
|
153
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
154
|
+
method: "PUT",
|
|
155
|
+
headers: createHeaders(true),
|
|
156
|
+
body: JSON.stringify(snakeCaseData)
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const error = await response.json().catch(() => ({}));
|
|
160
|
+
throw new Error(error.error || error.message || "Failed to update blog post");
|
|
161
|
+
}
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
return snakeToCamelObject(data);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return handleError(error);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Delete a blog post
|
|
170
|
+
*/
|
|
171
|
+
delete: async (id, language = config.defaultLanguage || "en") => {
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
174
|
+
method: "DELETE",
|
|
175
|
+
headers: createHeaders(true)
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const error = await response.json().catch(() => ({}));
|
|
179
|
+
throw new Error(error.error || "Failed to delete blog post");
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return handleError(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/utils/markdownRenderer.tsx
|
|
189
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
190
|
+
var renderMarkdown = (content) => {
|
|
191
|
+
const lines = content.trim().split("\n");
|
|
192
|
+
const elements = [];
|
|
193
|
+
let listItems = [];
|
|
194
|
+
const flushList = () => {
|
|
195
|
+
if (listItems.length > 0) {
|
|
196
|
+
elements.push(
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "list-disc list-inside space-y-2 text-muted-foreground mb-6 ml-4", children: listItems.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "leading-relaxed", children: item }, i)) }, `list-${elements.length}`)
|
|
198
|
+
);
|
|
199
|
+
listItems = [];
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
lines.forEach((line, index) => {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
if (trimmed.startsWith("## ")) {
|
|
205
|
+
flushList();
|
|
206
|
+
elements.push(
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-2xl sm:text-3xl font-bold text-foreground mt-10 mb-4", children: trimmed.replace("## ", "") }, index)
|
|
208
|
+
);
|
|
209
|
+
} else if (trimmed.startsWith("### ")) {
|
|
210
|
+
flushList();
|
|
211
|
+
elements.push(
|
|
212
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-xl sm:text-2xl font-semibold text-foreground mt-8 mb-3", children: trimmed.replace("### ", "") }, index)
|
|
213
|
+
);
|
|
214
|
+
} else if (trimmed.startsWith("- ")) {
|
|
215
|
+
listItems.push(trimmed.replace("- ", ""));
|
|
216
|
+
} else if (trimmed.startsWith("**") && trimmed.endsWith("**")) {
|
|
217
|
+
flushList();
|
|
218
|
+
elements.push(
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold text-foreground mb-2", children: trimmed.replace(/\*\*/g, "") }, index)
|
|
220
|
+
);
|
|
221
|
+
} else if (trimmed.length > 0) {
|
|
222
|
+
flushList();
|
|
223
|
+
const parts = trimmed.split(/(\*\*[^*]+\*\*)/g);
|
|
224
|
+
elements.push(
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground leading-relaxed mb-4", children: parts.map((part, i) => {
|
|
226
|
+
if (part.startsWith("**") && part.endsWith("**")) {
|
|
227
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "text-foreground font-semibold", children: part.replace(/\*\*/g, "") }, i);
|
|
228
|
+
}
|
|
229
|
+
return part;
|
|
230
|
+
}) }, index)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
flushList();
|
|
235
|
+
return elements;
|
|
236
|
+
};
|
|
237
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
238
|
+
0 && (module.exports = {
|
|
239
|
+
createBlogClient,
|
|
240
|
+
renderMarkdown
|
|
241
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/api/blogClient.ts
|
|
2
|
+
var camelToSnake = (str) => {
|
|
3
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
4
|
+
};
|
|
5
|
+
var camelToSnakeObject = (obj) => {
|
|
6
|
+
if (Array.isArray(obj)) {
|
|
7
|
+
return obj.map(camelToSnakeObject);
|
|
8
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
9
|
+
const newObj = {};
|
|
10
|
+
for (const key in obj) {
|
|
11
|
+
if (obj.hasOwnProperty(key)) {
|
|
12
|
+
const snakeKey = camelToSnake(key);
|
|
13
|
+
newObj[snakeKey] = camelToSnakeObject(obj[key]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return newObj;
|
|
17
|
+
}
|
|
18
|
+
return obj;
|
|
19
|
+
};
|
|
20
|
+
var snakeToCamel = (str) => {
|
|
21
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
22
|
+
};
|
|
23
|
+
var snakeToCamelObject = (obj) => {
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
return obj.map(snakeToCamelObject);
|
|
26
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
27
|
+
const newObj = {};
|
|
28
|
+
for (const key in obj) {
|
|
29
|
+
if (obj.hasOwnProperty(key)) {
|
|
30
|
+
const camelKey = snakeToCamel(key);
|
|
31
|
+
newObj[camelKey] = snakeToCamelObject(obj[key]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return newObj;
|
|
35
|
+
}
|
|
36
|
+
return obj;
|
|
37
|
+
};
|
|
38
|
+
var createBlogClient = (config) => {
|
|
39
|
+
const createHeaders = (requireAuth = false) => {
|
|
40
|
+
const headers = {
|
|
41
|
+
"Content-Type": "application/json"
|
|
42
|
+
};
|
|
43
|
+
if (requireAuth && config.getAuthToken) {
|
|
44
|
+
const token = config.getAuthToken();
|
|
45
|
+
if (token) {
|
|
46
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
};
|
|
51
|
+
const handleError = (error) => {
|
|
52
|
+
if (config.onError) {
|
|
53
|
+
config.onError(error);
|
|
54
|
+
} else {
|
|
55
|
+
console.error("Blog API Error:", error);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
/**
|
|
61
|
+
* Get all published blog posts for a language
|
|
62
|
+
*/
|
|
63
|
+
getAll: async (language = config.defaultLanguage || "en", includeDrafts = false) => {
|
|
64
|
+
try {
|
|
65
|
+
const url = `${config.apiBaseUrl}/blog-posts/?language=${language}${includeDrafts ? "&include_drafts=true" : ""}`;
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
headers: createHeaders(includeDrafts)
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const error = await response.json().catch(() => ({}));
|
|
72
|
+
throw new Error(error.error || "Failed to fetch blog posts");
|
|
73
|
+
}
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
return snakeToCamelObject(data);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return handleError(error);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Get a specific blog post by ID
|
|
82
|
+
*/
|
|
83
|
+
getById: async (id, language = config.defaultLanguage || "en") => {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: createHeaders(false)
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const error = await response.json().catch(() => ({}));
|
|
91
|
+
throw new Error(error.error || "Failed to fetch blog post");
|
|
92
|
+
}
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
return snakeToCamelObject(data);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return handleError(error);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* Create a new blog post
|
|
101
|
+
*/
|
|
102
|
+
create: async (post) => {
|
|
103
|
+
try {
|
|
104
|
+
const snakeCaseData = camelToSnakeObject(post);
|
|
105
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: createHeaders(true),
|
|
108
|
+
body: JSON.stringify(snakeCaseData)
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const error = await response.json().catch(() => ({}));
|
|
112
|
+
throw new Error(error.error || error.message || "Failed to create blog post");
|
|
113
|
+
}
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
return snakeToCamelObject(data);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return handleError(error);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* Update an existing blog post
|
|
122
|
+
*/
|
|
123
|
+
update: async (id, post, language = config.defaultLanguage || "en") => {
|
|
124
|
+
try {
|
|
125
|
+
const snakeCaseData = camelToSnakeObject(post);
|
|
126
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
headers: createHeaders(true),
|
|
129
|
+
body: JSON.stringify(snakeCaseData)
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = await response.json().catch(() => ({}));
|
|
133
|
+
throw new Error(error.error || error.message || "Failed to update blog post");
|
|
134
|
+
}
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
return snakeToCamelObject(data);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return handleError(error);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Delete a blog post
|
|
143
|
+
*/
|
|
144
|
+
delete: async (id, language = config.defaultLanguage || "en") => {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${config.apiBaseUrl}/blog-posts/${id}/?language=${language}`, {
|
|
147
|
+
method: "DELETE",
|
|
148
|
+
headers: createHeaders(true)
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const error = await response.json().catch(() => ({}));
|
|
152
|
+
throw new Error(error.error || "Failed to delete blog post");
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return handleError(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/utils/markdownRenderer.tsx
|
|
162
|
+
import { jsx } from "react/jsx-runtime";
|
|
163
|
+
var renderMarkdown = (content) => {
|
|
164
|
+
const lines = content.trim().split("\n");
|
|
165
|
+
const elements = [];
|
|
166
|
+
let listItems = [];
|
|
167
|
+
const flushList = () => {
|
|
168
|
+
if (listItems.length > 0) {
|
|
169
|
+
elements.push(
|
|
170
|
+
/* @__PURE__ */ jsx("ul", { className: "list-disc list-inside space-y-2 text-muted-foreground mb-6 ml-4", children: listItems.map((item, i) => /* @__PURE__ */ jsx("li", { className: "leading-relaxed", children: item }, i)) }, `list-${elements.length}`)
|
|
171
|
+
);
|
|
172
|
+
listItems = [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
lines.forEach((line, index) => {
|
|
176
|
+
const trimmed = line.trim();
|
|
177
|
+
if (trimmed.startsWith("## ")) {
|
|
178
|
+
flushList();
|
|
179
|
+
elements.push(
|
|
180
|
+
/* @__PURE__ */ jsx("h2", { className: "text-2xl sm:text-3xl font-bold text-foreground mt-10 mb-4", children: trimmed.replace("## ", "") }, index)
|
|
181
|
+
);
|
|
182
|
+
} else if (trimmed.startsWith("### ")) {
|
|
183
|
+
flushList();
|
|
184
|
+
elements.push(
|
|
185
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xl sm:text-2xl font-semibold text-foreground mt-8 mb-3", children: trimmed.replace("### ", "") }, index)
|
|
186
|
+
);
|
|
187
|
+
} else if (trimmed.startsWith("- ")) {
|
|
188
|
+
listItems.push(trimmed.replace("- ", ""));
|
|
189
|
+
} else if (trimmed.startsWith("**") && trimmed.endsWith("**")) {
|
|
190
|
+
flushList();
|
|
191
|
+
elements.push(
|
|
192
|
+
/* @__PURE__ */ jsx("p", { className: "font-semibold text-foreground mb-2", children: trimmed.replace(/\*\*/g, "") }, index)
|
|
193
|
+
);
|
|
194
|
+
} else if (trimmed.length > 0) {
|
|
195
|
+
flushList();
|
|
196
|
+
const parts = trimmed.split(/(\*\*[^*]+\*\*)/g);
|
|
197
|
+
elements.push(
|
|
198
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground leading-relaxed mb-4", children: parts.map((part, i) => {
|
|
199
|
+
if (part.startsWith("**") && part.endsWith("**")) {
|
|
200
|
+
return /* @__PURE__ */ jsx("strong", { className: "text-foreground font-semibold", children: part.replace(/\*\*/g, "") }, i);
|
|
201
|
+
}
|
|
202
|
+
return part;
|
|
203
|
+
}) }, index)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
flushList();
|
|
208
|
+
return elements;
|
|
209
|
+
};
|
|
210
|
+
export {
|
|
211
|
+
createBlogClient,
|
|
212
|
+
renderMarkdown
|
|
213
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@123resume/react-blog-system",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A complete, multi-language blog system for React applications with editor, management, and display components",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"module": "dist/index.esm.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
14
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react",
|
|
19
|
+
"blog",
|
|
20
|
+
"cms",
|
|
21
|
+
"multi-language",
|
|
22
|
+
"i18n",
|
|
23
|
+
"content-management",
|
|
24
|
+
"blog-editor",
|
|
25
|
+
"markdown"
|
|
26
|
+
],
|
|
27
|
+
"author": "123Resume",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "^18.0.0",
|
|
31
|
+
"react-dom": "^18.0.0",
|
|
32
|
+
"react-router-dom": "^6.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"lucide-react": "^0.263.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^18.2.0",
|
|
39
|
+
"@types/react-dom": "^18.2.0",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5.3.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|