@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 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.
@@ -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 };
@@ -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
+