@jhits/plugin-blog 0.0.16 → 0.0.18
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/dist/api/categories.d.ts.map +1 -1
- package/dist/api/categories.js +43 -12
- package/dist/api/handler.d.ts +1 -0
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +259 -32
- package/dist/hooks/useBlogs.d.ts +2 -0
- package/dist/hooks/useBlogs.d.ts.map +1 -1
- package/dist/hooks/useBlogs.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/lib/i18n.d.ts +14 -0
- package/dist/lib/i18n.d.ts.map +1 -0
- package/dist/lib/i18n.js +58 -0
- package/dist/lib/mappers/apiMapper.d.ts +18 -0
- package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
- package/dist/lib/mappers/apiMapper.js +1 -0
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/state/reducer.js +11 -6
- package/dist/state/types.d.ts +5 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/state/types.js +1 -0
- package/dist/types/post.d.ts +25 -0
- package/dist/types/post.d.ts.map +1 -1
- package/dist/utils/client.d.ts +2 -0
- package/dist/utils/client.d.ts.map +1 -1
- package/dist/utils/client.js +3 -1
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +130 -4
- package/dist/views/CanvasEditor/EditorHeader.d.ts +5 -1
- package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorHeader.js +23 -5
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +1 -1
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/usePostLoader.js +14 -4
- package/dist/views/PostManager/LanguageFlags.d.ts +11 -0
- package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -0
- package/dist/views/PostManager/LanguageFlags.js +60 -0
- package/dist/views/PostManager/PostCards.d.ts.map +1 -1
- package/dist/views/PostManager/PostCards.js +4 -1
- package/dist/views/PostManager/PostFilters.d.ts +4 -1
- package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
- package/dist/views/PostManager/PostFilters.js +13 -3
- package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
- package/dist/views/PostManager/PostManagerView.js +24 -3
- package/dist/views/PostManager/PostTable.d.ts.map +1 -1
- package/dist/views/PostManager/PostTable.js +4 -1
- package/package.json +4 -4
- package/src/api/categories.ts +58 -11
- package/src/api/handler.ts +286 -31
- package/src/hooks/useBlogs.ts +12 -1
- package/src/index.tsx +7 -3
- package/src/lib/i18n.ts +78 -0
- package/src/lib/mappers/apiMapper.ts +20 -0
- package/src/state/reducer.ts +12 -6
- package/src/state/types.ts +5 -0
- package/src/types/post.ts +28 -0
- package/src/utils/client.ts +4 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +164 -20
- package/src/views/CanvasEditor/EditorHeader.tsx +93 -18
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +15 -4
- package/src/views/PostManager/LanguageFlags.tsx +136 -0
- package/src/views/PostManager/PostCards.tsx +22 -12
- package/src/views/PostManager/PostFilters.tsx +38 -1
- package/src/views/PostManager/PostManagerView.tsx +25 -2
- package/src/views/PostManager/PostTable.tsx +12 -1
- package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +0 -81
package/src/api/handler.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface BlogApiConfig {
|
|
|
23
23
|
* GET /api/blogs - List all blog posts
|
|
24
24
|
* GET /api/blogs?admin=true - List all posts for admin (includes drafts)
|
|
25
25
|
* GET /api/blogs?status=published - Filter by status
|
|
26
|
+
* GET /api/blogs?language=en - Filter by language (falls back to nl if not found)
|
|
26
27
|
*/
|
|
27
28
|
export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
28
29
|
try {
|
|
@@ -31,6 +32,7 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
31
32
|
const skip = Number(url.searchParams.get('skip') ?? 0);
|
|
32
33
|
const statusFilter = url.searchParams.get('status');
|
|
33
34
|
const isAdminView = url.searchParams.get('admin') === 'true';
|
|
35
|
+
const requestedLanguage = url.searchParams.get('language') || 'nl';
|
|
34
36
|
|
|
35
37
|
const userId = await config.getUserId(req);
|
|
36
38
|
const dbConnection = await config.getDb();
|
|
@@ -51,11 +53,13 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
51
53
|
query = { authorId: userId };
|
|
52
54
|
}
|
|
53
55
|
} else {
|
|
54
|
-
// Public view: only published posts
|
|
56
|
+
// Public view: only published posts in the SPECIFIC language
|
|
57
|
+
// This ensures we only fetch blogs that actually have content for this language
|
|
55
58
|
query = {
|
|
56
|
-
|
|
59
|
+
[`languages.${requestedLanguage}.status`]: 'published',
|
|
57
60
|
'publicationData.date': { $lte: new Date() },
|
|
58
61
|
};
|
|
62
|
+
|
|
59
63
|
if (statusFilter && statusFilter !== 'published') {
|
|
60
64
|
// Non-admin can't filter by non-published status
|
|
61
65
|
return NextResponse.json({ error: 'Invalid status filter' }, { status: 400 });
|
|
@@ -72,14 +76,112 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
72
76
|
blogs.countDocuments(query),
|
|
73
77
|
]);
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Supported languages for fallback (in order of preference)
|
|
80
|
+
const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
|
|
81
|
+
|
|
82
|
+
const formatted = data.map((doc: any) => {
|
|
83
|
+
const languages = doc.languages || {};
|
|
84
|
+
|
|
85
|
+
// Only use exact language match public views - no fallback for
|
|
86
|
+
// This ensures visitors see content in their language only
|
|
87
|
+
const hasExactLanguage = !!languages[requestedLanguage];
|
|
88
|
+
|
|
89
|
+
// Skip this post if no exact language match (for public non-admin views)
|
|
90
|
+
if (!isAdminView && !hasExactLanguage) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const postPrimaryLang = doc.metadata?.lang || 'nl';
|
|
95
|
+
|
|
96
|
+
// Find the best available language for this post
|
|
97
|
+
let bestLanguage = requestedLanguage;
|
|
98
|
+
let isMissingTranslation = false;
|
|
99
|
+
|
|
100
|
+
if (!languages[requestedLanguage]) {
|
|
101
|
+
// For admin view, if specific language is requested, show it as missing instead of falling back
|
|
102
|
+
// This ensures the UI is "synced" with the selected language
|
|
103
|
+
if (isAdminView) {
|
|
104
|
+
isMissingTranslation = true;
|
|
105
|
+
} else {
|
|
106
|
+
// Public view still uses fallback or filtering
|
|
107
|
+
const fallbackLanguages = [postPrimaryLang, 'nl', 'en'];
|
|
108
|
+
for (const lang of fallbackLanguages) {
|
|
109
|
+
if (languages[lang]) {
|
|
110
|
+
bestLanguage = lang;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const langContent = languages[bestLanguage] || {};
|
|
118
|
+
const meta = langContent.metadata || {};
|
|
119
|
+
|
|
120
|
+
// Ensure all languages in doc have a status and updatedAt for the dashboard
|
|
121
|
+
const enrichedLanguages = { ...languages };
|
|
122
|
+
Object.keys(enrichedLanguages).forEach(lang => {
|
|
123
|
+
if (!enrichedLanguages[lang].status) {
|
|
124
|
+
enrichedLanguages[lang].status = doc.publicationData?.status === 'concept' ? 'draft' : (doc.publicationData?.status || 'draft');
|
|
125
|
+
}
|
|
126
|
+
if (!enrichedLanguages[lang].updatedAt) {
|
|
127
|
+
enrichedLanguages[lang].updatedAt = doc.updatedAt;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Language display names for placeholders
|
|
132
|
+
const langNames: Record<string, string> = {
|
|
133
|
+
nl: 'Dutch',
|
|
134
|
+
en: 'English',
|
|
135
|
+
sv: 'Swedish',
|
|
136
|
+
de: 'German',
|
|
137
|
+
fr: 'French',
|
|
138
|
+
es: 'Spanish'
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const displayTitle = isMissingTranslation
|
|
142
|
+
? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
|
|
143
|
+
: (meta.title || doc.title || '');
|
|
144
|
+
|
|
145
|
+
const displayStatus = isMissingTranslation
|
|
146
|
+
? 'not-translated'
|
|
147
|
+
: (langContent.status || doc.publicationData?.status || 'concept');
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...doc,
|
|
151
|
+
_id: doc._id.toString(),
|
|
152
|
+
title: displayTitle,
|
|
153
|
+
summary: isMissingTranslation ? '' : (meta.excerpt || doc.summary || ''),
|
|
154
|
+
contentBlocks: isMissingTranslation ? [] : (langContent.blocks || doc.contentBlocks || doc.blocks || []),
|
|
155
|
+
image: isMissingTranslation ? null : (meta.featuredImage || doc.image),
|
|
156
|
+
categoryTags: isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
|
|
157
|
+
category: meta.categories[0] || '',
|
|
158
|
+
tags: meta.tags || doc.categoryTags?.tags || []
|
|
159
|
+
} : (doc.categoryTags || { category: '', tags: [] })),
|
|
160
|
+
publicationData: {
|
|
161
|
+
...doc.publicationData,
|
|
162
|
+
status: displayStatus,
|
|
163
|
+
},
|
|
164
|
+
seo: isMissingTranslation ? {} : (meta.seo || doc.seo || {}),
|
|
165
|
+
lang: bestLanguage,
|
|
166
|
+
isMissingTranslation,
|
|
167
|
+
requestedLanguage,
|
|
168
|
+
availableLanguages: Object.keys(languages),
|
|
169
|
+
languages: enrichedLanguages,
|
|
170
|
+
updatedAt: isMissingTranslation ? doc.updatedAt : (langContent.updatedAt || doc.updatedAt),
|
|
171
|
+
status: displayStatus,
|
|
172
|
+
};
|
|
173
|
+
}).filter(Boolean); // Remove null entries
|
|
174
|
+
|
|
175
|
+
// Sort by updatedAt descending so the UI order makes sense
|
|
176
|
+
formatted.sort((a: any, b: any) => {
|
|
177
|
+
const dateA = new Date(a.updatedAt).getTime();
|
|
178
|
+
const dateB = new Date(b.updatedAt).getTime();
|
|
179
|
+
return dateB - dateA;
|
|
180
|
+
});
|
|
79
181
|
|
|
80
182
|
return NextResponse.json({
|
|
81
183
|
blogs: formatted,
|
|
82
|
-
total:
|
|
184
|
+
total: formatted.length,
|
|
83
185
|
});
|
|
84
186
|
} catch (err: any) {
|
|
85
187
|
console.error('[BlogAPI] GET error:', err);
|
|
@@ -95,6 +197,9 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
95
197
|
*/
|
|
96
198
|
export async function POST(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
|
|
97
199
|
try {
|
|
200
|
+
const url = new URL(req.url);
|
|
201
|
+
const language = url.searchParams.get('language') || 'nl';
|
|
202
|
+
|
|
98
203
|
const userId = await config.getUserId(req);
|
|
99
204
|
if (!userId) {
|
|
100
205
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
@@ -181,6 +286,24 @@ export async function POST(req: NextRequest, config: BlogApiConfig): Promise<Nex
|
|
|
181
286
|
seo: seo || { title: '', description: '' },
|
|
182
287
|
slug,
|
|
183
288
|
authorId: userId,
|
|
289
|
+
languages: {
|
|
290
|
+
[language]: {
|
|
291
|
+
blocks: contentBlocks || [],
|
|
292
|
+
metadata: {
|
|
293
|
+
title: title.trim(),
|
|
294
|
+
excerpt: (summary || '').trim(),
|
|
295
|
+
featuredImage: image,
|
|
296
|
+
categories: categoryTags?.category ? [categoryTags.category] : [],
|
|
297
|
+
tags: categoryTags?.tags || [],
|
|
298
|
+
seo: seo || {},
|
|
299
|
+
},
|
|
300
|
+
updatedAt: new Date().toISOString(),
|
|
301
|
+
status: finalStatus,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
metadata: {
|
|
305
|
+
lang: language,
|
|
306
|
+
},
|
|
184
307
|
createdAt: new Date(),
|
|
185
308
|
updatedAt: new Date(),
|
|
186
309
|
};
|
|
@@ -210,6 +333,9 @@ export async function GET_BY_SLUG(
|
|
|
210
333
|
config: BlogApiConfig
|
|
211
334
|
): Promise<NextResponse> {
|
|
212
335
|
try {
|
|
336
|
+
const url = new URL(req.url);
|
|
337
|
+
const requestedLanguage = url.searchParams.get('language') || 'nl';
|
|
338
|
+
|
|
213
339
|
const userId = await config.getUserId(req);
|
|
214
340
|
const dbConnection = await config.getDb();
|
|
215
341
|
const db = dbConnection.db();
|
|
@@ -222,16 +348,110 @@ export async function GET_BY_SLUG(
|
|
|
222
348
|
}
|
|
223
349
|
|
|
224
350
|
// Security check
|
|
225
|
-
const isPublished = blog.publicationData?.status === 'published';
|
|
226
351
|
const isAuthor = userId && blog.authorId === userId;
|
|
352
|
+
const isAdminView = url.searchParams.get('admin') === 'true';
|
|
353
|
+
|
|
354
|
+
// For public view, we must have a published version in the requested language
|
|
355
|
+
if (!isAdminView && !isAuthor) {
|
|
356
|
+
const langData = blog.languages?.[requestedLanguage];
|
|
357
|
+
if (!langData || langData.status !== 'published') {
|
|
358
|
+
return NextResponse.json({ error: 'Blog post not available in this language' }, { status: 404 });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Also check publication date
|
|
362
|
+
if (blog.publicationData?.date && new Date(blog.publicationData.date) > new Date()) {
|
|
363
|
+
return NextResponse.json({ error: 'Blog post not yet published' }, { status: 404 });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
227
366
|
|
|
228
|
-
|
|
229
|
-
|
|
367
|
+
const languages = blog.languages || {};
|
|
368
|
+
const postPrimaryLang = blog.metadata?.lang || 'nl';
|
|
369
|
+
|
|
370
|
+
// Find the best available language for this post
|
|
371
|
+
let bestLanguage = requestedLanguage;
|
|
372
|
+
let isMissingTranslation = false;
|
|
373
|
+
|
|
374
|
+
if (!languages[requestedLanguage]) {
|
|
375
|
+
if (isAdminView) {
|
|
376
|
+
isMissingTranslation = true;
|
|
377
|
+
} else {
|
|
378
|
+
// Try fallback languages in order
|
|
379
|
+
const fallbackLanguages = [requestedLanguage, 'nl', 'en'];
|
|
380
|
+
for (const lang of fallbackLanguages) {
|
|
381
|
+
if (languages[lang]) {
|
|
382
|
+
bestLanguage = lang;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// If still no match, use primary language from metadata
|
|
387
|
+
if (!languages[bestLanguage] && languages[postPrimaryLang]) {
|
|
388
|
+
bestLanguage = postPrimaryLang;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
230
391
|
}
|
|
392
|
+
|
|
393
|
+
const langContent = languages[bestLanguage] || {};
|
|
394
|
+
const meta = langContent.metadata || {};
|
|
395
|
+
|
|
396
|
+
// Language display names for placeholders
|
|
397
|
+
const langNames: Record<string, string> = {
|
|
398
|
+
nl: 'Dutch',
|
|
399
|
+
en: 'English',
|
|
400
|
+
sv: 'Swedish',
|
|
401
|
+
de: 'German',
|
|
402
|
+
fr: 'French',
|
|
403
|
+
es: 'Spanish'
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
let title = isMissingTranslation
|
|
407
|
+
? `(No ${langNames[requestedLanguage] || requestedLanguage.toUpperCase()} translation)`
|
|
408
|
+
: (meta.title || blog.title || '');
|
|
409
|
+
let summary = isMissingTranslation ? '' : (meta.excerpt || blog.summary || '');
|
|
410
|
+
let contentBlocks = isMissingTranslation ? [] : (langContent.blocks || blog.contentBlocks || blog.blocks || []);
|
|
411
|
+
let image = isMissingTranslation ? null : (meta.featuredImage || blog.image);
|
|
412
|
+
let categoryTags = isMissingTranslation ? { category: '', tags: [] } : (meta.categories ? {
|
|
413
|
+
category: meta.categories[0] || '',
|
|
414
|
+
tags: meta.tags || blog.categoryTags?.tags || []
|
|
415
|
+
} : (blog.categoryTags || { category: '', tags: [] }));
|
|
416
|
+
let seo = isMissingTranslation ? {} : (meta.seo || blog.seo || {});
|
|
417
|
+
let metadata = { ...blog.metadata, ...meta, lang: bestLanguage };
|
|
418
|
+
|
|
419
|
+
// Ensure all languages in doc have a status and updatedAt for the dashboard
|
|
420
|
+
const enrichedLanguages = { ...languages };
|
|
421
|
+
Object.keys(enrichedLanguages).forEach(lang => {
|
|
422
|
+
if (!enrichedLanguages[lang].status) {
|
|
423
|
+
enrichedLanguages[lang].status = blog.publicationData?.status === 'concept' ? 'draft' : (blog.publicationData?.status || 'draft');
|
|
424
|
+
}
|
|
425
|
+
if (!enrichedLanguages[lang].updatedAt) {
|
|
426
|
+
enrichedLanguages[lang].updatedAt = blog.updatedAt;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const displayStatus = isMissingTranslation
|
|
431
|
+
? 'not-translated'
|
|
432
|
+
: (langContent.status || blog.publicationData?.status || 'concept');
|
|
231
433
|
|
|
232
434
|
return NextResponse.json({
|
|
233
435
|
...blog,
|
|
234
436
|
_id: blog._id.toString(),
|
|
437
|
+
title,
|
|
438
|
+
summary,
|
|
439
|
+
contentBlocks,
|
|
440
|
+
image,
|
|
441
|
+
categoryTags,
|
|
442
|
+
publicationData: {
|
|
443
|
+
...blog.publicationData,
|
|
444
|
+
status: displayStatus,
|
|
445
|
+
},
|
|
446
|
+
seo,
|
|
447
|
+
metadata,
|
|
448
|
+
languages: enrichedLanguages,
|
|
449
|
+
availableLanguages: Object.keys(languages),
|
|
450
|
+
lang: bestLanguage,
|
|
451
|
+
isMissingTranslation,
|
|
452
|
+
requestedLanguage,
|
|
453
|
+
updatedAt: isMissingTranslation ? blog.updatedAt : (langContent.updatedAt || blog.updatedAt),
|
|
454
|
+
status: displayStatus,
|
|
235
455
|
});
|
|
236
456
|
} catch (err: any) {
|
|
237
457
|
console.error('[BlogAPI] GET_BY_SLUG error:', err);
|
|
@@ -251,6 +471,9 @@ export async function PUT_BY_SLUG(
|
|
|
251
471
|
config: BlogApiConfig
|
|
252
472
|
): Promise<NextResponse> {
|
|
253
473
|
try {
|
|
474
|
+
const url = new URL(req.url);
|
|
475
|
+
const language = url.searchParams.get('language') || 'nl';
|
|
476
|
+
|
|
254
477
|
const userId = await config.getUserId(req);
|
|
255
478
|
if (!userId) {
|
|
256
479
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
@@ -324,15 +547,10 @@ export async function PUT_BY_SLUG(
|
|
|
324
547
|
}
|
|
325
548
|
}
|
|
326
549
|
|
|
327
|
-
// Slug logic
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
finalSlug = slugify(title);
|
|
331
|
-
} else if (publicationData?.status === 'concept' && !slug.includes('-draft-')) {
|
|
332
|
-
finalSlug = `${slugify(title)}-draft-${Date.now().toString().slice(-4)}`;
|
|
333
|
-
}
|
|
550
|
+
// Slug logic - DON'T change slug for existing posts, keep original
|
|
551
|
+
// The slug should remain constant across all language versions
|
|
552
|
+
const finalSlug = slug;
|
|
334
553
|
|
|
335
|
-
// Update data
|
|
336
554
|
// Determine the final status: if publishing, set to 'published', otherwise preserve or convert draft to concept
|
|
337
555
|
let finalStatus = publicationData?.status;
|
|
338
556
|
if (isPublishing) {
|
|
@@ -341,24 +559,61 @@ export async function PUT_BY_SLUG(
|
|
|
341
559
|
finalStatus = 'concept';
|
|
342
560
|
}
|
|
343
561
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
562
|
+
// Preserve existing languages or initialize
|
|
563
|
+
const existingLanguages = existingBlog.languages || {};
|
|
564
|
+
const primaryLanguage = existingBlog.metadata?.lang || 'nl';
|
|
565
|
+
|
|
566
|
+
// Update the specific language content
|
|
567
|
+
// Only the language being saved gets a new updatedAt timestamp
|
|
568
|
+
const now = new Date();
|
|
569
|
+
const updatedLanguages = {
|
|
570
|
+
...existingLanguages,
|
|
571
|
+
[language]: {
|
|
572
|
+
blocks: contentBlocks || [],
|
|
573
|
+
metadata: {
|
|
574
|
+
title: title.trim(),
|
|
575
|
+
excerpt: (summary || '').trim(),
|
|
576
|
+
featuredImage: image,
|
|
577
|
+
categories: categoryTags?.category ? [categoryTags.category] : [],
|
|
578
|
+
tags: categoryTags?.tags || [],
|
|
579
|
+
seo: seo || {},
|
|
580
|
+
},
|
|
581
|
+
updatedAt: now.toISOString(),
|
|
582
|
+
status: finalStatus,
|
|
353
583
|
},
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// For root-level fields, only update if this is the primary language
|
|
587
|
+
// Otherwise preserve existing root values to maintain backward compatibility
|
|
588
|
+
const isPrimaryLanguage = language === primaryLanguage || !existingLanguages[primaryLanguage];
|
|
589
|
+
|
|
590
|
+
const updateData: any = {
|
|
591
|
+
// Only update root-level title/summary if saving in primary language
|
|
592
|
+
// This maintains backward compatibility
|
|
593
|
+
...(isPrimaryLanguage ? {
|
|
594
|
+
title: title.trim(),
|
|
595
|
+
summary: (summary || '').trim(),
|
|
596
|
+
contentBlocks: contentBlocks || [],
|
|
597
|
+
content: content || [],
|
|
598
|
+
image: image || {},
|
|
599
|
+
categoryTags: {
|
|
600
|
+
category: categoryTags?.category?.trim() || '',
|
|
601
|
+
tags: categoryTags?.tags || [],
|
|
602
|
+
},
|
|
603
|
+
seo: seo || {},
|
|
604
|
+
} : {}),
|
|
354
605
|
publicationData: {
|
|
606
|
+
...existingBlog.publicationData,
|
|
355
607
|
...publicationData,
|
|
356
608
|
status: finalStatus,
|
|
357
|
-
date: publicationData?.date ? new Date(publicationData.date) : new Date(),
|
|
609
|
+
date: publicationData?.date ? new Date(publicationData.date) : existingBlog.publicationData?.date || new Date(),
|
|
358
610
|
},
|
|
359
|
-
seo: seo || {},
|
|
360
|
-
slug: finalSlug,
|
|
361
611
|
authorId: userId,
|
|
612
|
+
languages: updatedLanguages,
|
|
613
|
+
metadata: {
|
|
614
|
+
...existingBlog.metadata,
|
|
615
|
+
lang: primaryLanguage,
|
|
616
|
+
},
|
|
362
617
|
updatedAt: new Date(),
|
|
363
618
|
};
|
|
364
619
|
|
|
@@ -366,7 +621,7 @@ export async function PUT_BY_SLUG(
|
|
|
366
621
|
|
|
367
622
|
return NextResponse.json({
|
|
368
623
|
message: 'Blog updated successfully',
|
|
369
|
-
slug: finalSlug
|
|
624
|
+
slug: slug, // Return original slug, not finalSlug
|
|
370
625
|
});
|
|
371
626
|
} catch (err: any) {
|
|
372
627
|
console.error('[BlogAPI] PUT_BY_SLUG error:', err);
|
package/src/hooks/useBlogs.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface UseBlogsOptions {
|
|
|
14
14
|
skip?: number;
|
|
15
15
|
/** Filter by status (published, draft, concept) */
|
|
16
16
|
status?: string;
|
|
17
|
+
/** Filter by language */
|
|
18
|
+
language?: string;
|
|
17
19
|
/** Whether to fetch all posts for admin (includes drafts) */
|
|
18
20
|
admin?: boolean;
|
|
19
21
|
/** API base URL (default: '/api/blogs') */
|
|
@@ -46,6 +48,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
|
|
|
46
48
|
limit = 10,
|
|
47
49
|
skip = 0,
|
|
48
50
|
status,
|
|
51
|
+
language,
|
|
49
52
|
admin = false,
|
|
50
53
|
apiBaseUrl = '/api/plugin-blog',
|
|
51
54
|
} = options;
|
|
@@ -56,6 +59,13 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
|
|
|
56
59
|
const [total, setTotal] = useState(0);
|
|
57
60
|
|
|
58
61
|
const fetchBlogs = async () => {
|
|
62
|
+
// If language is expected but not provided, wait for it to be ready
|
|
63
|
+
// (Prevents fetching all languages if the hook is called with a language but it's not yet loaded)
|
|
64
|
+
if (!language && !admin) {
|
|
65
|
+
// In public view, we always want a specific language
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
try {
|
|
60
70
|
setLoading(true);
|
|
61
71
|
setError(null);
|
|
@@ -64,6 +74,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
|
|
|
64
74
|
if (limit) params.set('limit', limit.toString());
|
|
65
75
|
if (skip) params.set('skip', skip.toString());
|
|
66
76
|
if (status) params.set('status', status);
|
|
77
|
+
if (language) params.set('language', language);
|
|
67
78
|
if (admin) params.set('admin', 'true');
|
|
68
79
|
|
|
69
80
|
const url = `${apiBaseUrl}?${params.toString()}`;
|
|
@@ -110,7 +121,7 @@ export function useBlogs(options: UseBlogsOptions = {}): UseBlogsResult {
|
|
|
110
121
|
|
|
111
122
|
useEffect(() => {
|
|
112
123
|
fetchBlogs();
|
|
113
|
-
}, [limit, skip, status, admin, apiBaseUrl]);
|
|
124
|
+
}, [limit, skip, status, language, admin, apiBaseUrl]);
|
|
114
125
|
|
|
115
126
|
return {
|
|
116
127
|
blogs,
|
package/src/index.tsx
CHANGED
|
@@ -186,9 +186,10 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
186
186
|
if (!originalSlug) {
|
|
187
187
|
throw new Error('Cannot save: no post identifier available. Please reload the page.');
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
const language = state.currentLanguage || locale;
|
|
190
|
+
console.log('[BlogPlugin] Saving post with slug:', originalSlug, 'language:', language);
|
|
190
191
|
const apiData = editorStateToAPI(state, undefined, heroBlock);
|
|
191
|
-
const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
|
|
192
|
+
const response = await fetch(`/api/plugin-blog/${originalSlug}?language=${language}`, {
|
|
192
193
|
method: 'PUT',
|
|
193
194
|
headers: { 'Content-Type': 'application/json' },
|
|
194
195
|
credentials: 'include', // Include cookies for authentication
|
|
@@ -229,8 +230,9 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
229
230
|
backgroundColors={backgroundColors}
|
|
230
231
|
onSave={async (state) => {
|
|
231
232
|
// Save to API - create new post
|
|
233
|
+
const language = state.currentLanguage || locale;
|
|
232
234
|
const apiData = editorStateToAPI(state);
|
|
233
|
-
const response = await fetch(
|
|
235
|
+
const response = await fetch(`/api/plugin-blog/new?language=${language}`, {
|
|
234
236
|
method: 'POST',
|
|
235
237
|
headers: { 'Content-Type': 'application/json' },
|
|
236
238
|
credentials: 'include', // Include cookies for authentication
|
|
@@ -293,6 +295,8 @@ export type {
|
|
|
293
295
|
BlogPost,
|
|
294
296
|
PostListItem,
|
|
295
297
|
PostFilterOptions,
|
|
298
|
+
BlogPostLanguageContent,
|
|
299
|
+
BlogPostLanguages,
|
|
296
300
|
} from './types/post';
|
|
297
301
|
|
|
298
302
|
// Export initialization utility for easy setup
|
package/src/lib/i18n.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n Utility
|
|
3
|
+
* Simple translation loader for the blog plugin
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
function getLocalesDir(): string {
|
|
10
|
+
const possiblePaths = [
|
|
11
|
+
path.join(process.cwd(), 'node_modules', '@jhits', 'plugin-blog', 'data', 'locales'),
|
|
12
|
+
path.join(__dirname, '..', 'data', 'locales'),
|
|
13
|
+
path.join(__dirname, '..', '..', 'data', 'locales'),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const localesPath of possiblePaths) {
|
|
17
|
+
if (fs.existsSync(localesPath)) {
|
|
18
|
+
return localesPath;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return possiblePaths[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const localesDir = getLocalesDir();
|
|
26
|
+
const cache: Record<string, any> = {};
|
|
27
|
+
|
|
28
|
+
function loadLocale(locale: string): any {
|
|
29
|
+
if (cache[locale]) {
|
|
30
|
+
return cache[locale];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const localePath = path.join(localesDir, locale, 'common.json');
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(localePath)) {
|
|
37
|
+
const content = fs.readFileSync(localePath, 'utf-8');
|
|
38
|
+
cache[locale] = JSON.parse(content);
|
|
39
|
+
return cache[locale];
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`[i18n] Failed to load locale ${locale}:`, error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getTranslations(locale: string): any {
|
|
49
|
+
const translations = loadLocale(locale);
|
|
50
|
+
|
|
51
|
+
if (translations) {
|
|
52
|
+
return translations;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fallback = loadLocale('en');
|
|
56
|
+
return fallback || {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getEditorTranslations(language: string): {
|
|
60
|
+
language: string;
|
|
61
|
+
selectLanguage: string;
|
|
62
|
+
addLanguage: string;
|
|
63
|
+
switchLanguage: string;
|
|
64
|
+
availableLanguages: string;
|
|
65
|
+
primaryLanguage: string;
|
|
66
|
+
} {
|
|
67
|
+
const translations = getTranslations(language);
|
|
68
|
+
const enTranslations = getTranslations('en');
|
|
69
|
+
|
|
70
|
+
return translations.editor || enTranslations.editor || {
|
|
71
|
+
language: 'Language',
|
|
72
|
+
selectLanguage: 'Select Language',
|
|
73
|
+
addLanguage: 'Add Language',
|
|
74
|
+
switchLanguage: 'Switch Language',
|
|
75
|
+
availableLanguages: 'Available Languages',
|
|
76
|
+
primaryLanguage: 'Primary Language',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -43,6 +43,25 @@ export interface APIBlogDocument {
|
|
|
43
43
|
authorId?: string;
|
|
44
44
|
createdAt?: string | Date;
|
|
45
45
|
updatedAt?: string | Date;
|
|
46
|
+
// Multilingual fields
|
|
47
|
+
languages?: {
|
|
48
|
+
[key: string]: {
|
|
49
|
+
blocks: Block[];
|
|
50
|
+
metadata: {
|
|
51
|
+
title?: string;
|
|
52
|
+
excerpt?: string;
|
|
53
|
+
featuredImage?: any;
|
|
54
|
+
categories?: string[];
|
|
55
|
+
tags?: string[];
|
|
56
|
+
seo?: any;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
availableLanguages?: string[];
|
|
61
|
+
lang?: string;
|
|
62
|
+
metadata?: {
|
|
63
|
+
lang?: string;
|
|
64
|
+
};
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
/**
|
|
@@ -108,6 +127,7 @@ export function apiToBlogPost(doc: APIBlogDocument): BlogPost {
|
|
|
108
127
|
seo,
|
|
109
128
|
publication,
|
|
110
129
|
metadata,
|
|
130
|
+
languages: doc.languages,
|
|
111
131
|
createdAt: doc.createdAt
|
|
112
132
|
? (typeof doc.createdAt === 'string' ? doc.createdAt : doc.createdAt.toISOString())
|
|
113
133
|
: new Date().toISOString(),
|
package/src/state/reducer.ts
CHANGED
|
@@ -652,12 +652,12 @@ export function editorReducer(
|
|
|
652
652
|
const post = action.payload;
|
|
653
653
|
return {
|
|
654
654
|
...state,
|
|
655
|
-
blocks: post.blocks,
|
|
656
|
-
title: post.title,
|
|
657
|
-
slug: post.slug,
|
|
658
|
-
seo: post.seo,
|
|
659
|
-
metadata: post.metadata,
|
|
660
|
-
status: post.publication
|
|
655
|
+
blocks: post.blocks || [],
|
|
656
|
+
title: post.title || '',
|
|
657
|
+
slug: post.slug || '',
|
|
658
|
+
seo: post.seo || {},
|
|
659
|
+
metadata: post.metadata || {},
|
|
660
|
+
status: post.publication?.status || 'draft',
|
|
661
661
|
postId: post.id,
|
|
662
662
|
isDirty: false,
|
|
663
663
|
selectedBlockId: null,
|
|
@@ -681,6 +681,12 @@ export function editorReducer(
|
|
|
681
681
|
isDirty: true,
|
|
682
682
|
};
|
|
683
683
|
|
|
684
|
+
case 'SET_CURRENT_LANGUAGE':
|
|
685
|
+
return {
|
|
686
|
+
...state,
|
|
687
|
+
currentLanguage: action.payload,
|
|
688
|
+
};
|
|
689
|
+
|
|
684
690
|
case 'UNDO':
|
|
685
691
|
case 'REDO':
|
|
686
692
|
case 'SAVE_HISTORY':
|
package/src/state/types.ts
CHANGED
|
@@ -43,6 +43,9 @@ export interface EditorState {
|
|
|
43
43
|
|
|
44
44
|
/** Post ID (if editing existing post) */
|
|
45
45
|
postId: string | null;
|
|
46
|
+
|
|
47
|
+
/** Currently selected language for editing */
|
|
48
|
+
currentLanguage: string | null;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/**
|
|
@@ -68,6 +71,7 @@ export type EditorAction =
|
|
|
68
71
|
| { type: 'RESET_EDITOR' }
|
|
69
72
|
| { type: 'MARK_CLEAN' }
|
|
70
73
|
| { type: 'MARK_DIRTY' }
|
|
74
|
+
| { type: 'SET_CURRENT_LANGUAGE'; payload: string }
|
|
71
75
|
| { type: 'UNDO' }
|
|
72
76
|
| { type: 'REDO' }
|
|
73
77
|
| { type: 'SAVE_HISTORY' };
|
|
@@ -156,5 +160,6 @@ export const initialEditorState: EditorState = {
|
|
|
156
160
|
selectedBlockId: null,
|
|
157
161
|
draggedBlockId: null,
|
|
158
162
|
postId: null,
|
|
163
|
+
currentLanguage: null,
|
|
159
164
|
};
|
|
160
165
|
|