@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.
Files changed (67) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +43 -12
  3. package/dist/api/handler.d.ts +1 -0
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +259 -32
  6. package/dist/hooks/useBlogs.d.ts +2 -0
  7. package/dist/hooks/useBlogs.d.ts.map +1 -1
  8. package/dist/hooks/useBlogs.js +10 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -3
  12. package/dist/lib/i18n.d.ts +14 -0
  13. package/dist/lib/i18n.d.ts.map +1 -0
  14. package/dist/lib/i18n.js +58 -0
  15. package/dist/lib/mappers/apiMapper.d.ts +18 -0
  16. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  17. package/dist/lib/mappers/apiMapper.js +1 -0
  18. package/dist/state/reducer.d.ts.map +1 -1
  19. package/dist/state/reducer.js +11 -6
  20. package/dist/state/types.d.ts +5 -0
  21. package/dist/state/types.d.ts.map +1 -1
  22. package/dist/state/types.js +1 -0
  23. package/dist/types/post.d.ts +25 -0
  24. package/dist/types/post.d.ts.map +1 -1
  25. package/dist/utils/client.d.ts +2 -0
  26. package/dist/utils/client.d.ts.map +1 -1
  27. package/dist/utils/client.js +3 -1
  28. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  29. package/dist/views/CanvasEditor/CanvasEditorView.js +130 -4
  30. package/dist/views/CanvasEditor/EditorHeader.d.ts +5 -1
  31. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/EditorHeader.js +23 -5
  33. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +1 -1
  34. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/hooks/usePostLoader.js +14 -4
  36. package/dist/views/PostManager/LanguageFlags.d.ts +11 -0
  37. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -0
  38. package/dist/views/PostManager/LanguageFlags.js +60 -0
  39. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  40. package/dist/views/PostManager/PostCards.js +4 -1
  41. package/dist/views/PostManager/PostFilters.d.ts +4 -1
  42. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  43. package/dist/views/PostManager/PostFilters.js +13 -3
  44. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  45. package/dist/views/PostManager/PostManagerView.js +24 -3
  46. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  47. package/dist/views/PostManager/PostTable.js +4 -1
  48. package/package.json +4 -4
  49. package/src/api/categories.ts +58 -11
  50. package/src/api/handler.ts +286 -31
  51. package/src/hooks/useBlogs.ts +12 -1
  52. package/src/index.tsx +7 -3
  53. package/src/lib/i18n.ts +78 -0
  54. package/src/lib/mappers/apiMapper.ts +20 -0
  55. package/src/state/reducer.ts +12 -6
  56. package/src/state/types.ts +5 -0
  57. package/src/types/post.ts +28 -0
  58. package/src/utils/client.ts +4 -0
  59. package/src/views/CanvasEditor/CanvasEditorView.tsx +164 -20
  60. package/src/views/CanvasEditor/EditorHeader.tsx +93 -18
  61. package/src/views/CanvasEditor/hooks/usePostLoader.ts +15 -4
  62. package/src/views/PostManager/LanguageFlags.tsx +136 -0
  63. package/src/views/PostManager/PostCards.tsx +22 -12
  64. package/src/views/PostManager/PostFilters.tsx +38 -1
  65. package/src/views/PostManager/PostManagerView.tsx +25 -2
  66. package/src/views/PostManager/PostTable.tsx +12 -1
  67. package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +0 -81
package/src/types/post.ts CHANGED
@@ -68,6 +68,8 @@ export interface PrivacySettings {
68
68
  * Post Metadata
69
69
  */
70
70
  export interface PostMetadata {
71
+ /** Language-specific title */
72
+ title?: string;
71
73
  /** Featured image */
72
74
  featuredImage?: {
73
75
  id?: string; // Image ID (preferred, for plugin-images system)
@@ -123,6 +125,9 @@ export interface BlogPost {
123
125
  /** Additional metadata */
124
126
  metadata: PostMetadata;
125
127
 
128
+ /** Content per language (for multi-language posts) */
129
+ languages?: BlogPostLanguages;
130
+
126
131
  /** Creation timestamp */
127
132
  createdAt: string;
128
133
 
@@ -148,6 +153,9 @@ export interface PostListItem {
148
153
  authorId?: string;
149
154
  updatedAt: string;
150
155
  category?: string;
156
+ lang?: string;
157
+ availableLanguages?: string[];
158
+ languages?: BlogPostLanguages;
151
159
  }
152
160
 
153
161
  /**
@@ -159,6 +167,7 @@ export interface PostFilterOptions {
159
167
  tag?: string;
160
168
  authorId?: string;
161
169
  search?: string;
170
+ language?: string;
162
171
  dateFrom?: string;
163
172
  dateTo?: string;
164
173
  limit?: number;
@@ -167,3 +176,22 @@ export interface PostFilterOptions {
167
176
  sortOrder?: 'asc' | 'desc';
168
177
  }
169
178
 
179
+ /**
180
+ * Blog post language content (blocks + metadata per language)
181
+ */
182
+ export interface BlogPostLanguageContent {
183
+ blocks: Block[];
184
+ metadata: PostMetadata;
185
+ /** Per-language last update timestamp */
186
+ updatedAt?: string;
187
+ /** Per-language publication status */
188
+ status?: PostStatus;
189
+ }
190
+
191
+ /**
192
+ * Languages object storing content per language
193
+ */
194
+ export interface BlogPostLanguages {
195
+ [key: string]: BlogPostLanguageContent;
196
+ }
197
+
@@ -13,6 +13,8 @@ export interface FetchBlogsOptions {
13
13
  skip?: number;
14
14
  /** Filter by status (published, draft, concept) */
15
15
  status?: string;
16
+ /** Filter by language */
17
+ language?: string;
16
18
  /** Whether to fetch all posts for admin (includes drafts) */
17
19
  admin?: boolean;
18
20
  /** API base URL (default: '/api/blogs') */
@@ -39,6 +41,7 @@ export async function fetchBlogs(options: FetchBlogsOptions = {}): Promise<Fetch
39
41
  limit = 10,
40
42
  skip = 0,
41
43
  status,
44
+ language,
42
45
  admin = false,
43
46
  apiBaseUrl = '/api/plugin-blog',
44
47
  } = options;
@@ -47,6 +50,7 @@ export async function fetchBlogs(options: FetchBlogsOptions = {}): Promise<Fetch
47
50
  if (limit) params.set('limit', limit.toString());
48
51
  if (skip) params.set('skip', skip.toString());
49
52
  if (status) params.set('status', status);
53
+ if (language) params.set('language', language);
50
54
  if (admin) params.set('admin', 'true');
51
55
 
52
56
  const url = `${apiBaseUrl}?${params.toString()}`;
@@ -34,45 +34,182 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
34
34
  const [isSaving, setIsSaving] = useState(false);
35
35
  const [saveError, setSaveError] = useState<string | null>(null);
36
36
 
37
+ // Language state for multilingual support
38
+ const [primaryLanguage, setPrimaryLanguage] = useState<string>(locale || 'en');
39
+ const [isLoadingLanguage, setIsLoadingLanguage] = useState(true);
40
+ const [availableLanguages, setAvailableLanguages] = useState<string[]>([locale || 'en']);
41
+ const [currentLanguage, setCurrentLanguage] = useState<string>(locale || 'en');
42
+
37
43
  // Get registered blocks
38
44
  const registeredBlocks = useRegisteredBlocks();
39
45
 
40
46
  // Hero block management
41
47
  const { heroBlock, setHeroBlock, heroBlockDefinition } = useHeroBlock(state, registeredBlocks);
42
48
 
43
- // Post loading
49
+ // Post loading - wait for language settings to be loaded first
44
50
  const { isLoadingPost } = usePostLoader(
45
51
  postId,
46
52
  state.postId,
47
53
  (post) => {
48
54
  helpers.loadPost(post);
55
+ // Don't reset current language when loading - preserve user's selection
56
+ // This allows switching to a new language even if no content exists yet
57
+ // Update available languages from post's languages object
58
+ if (post.languages && Object.keys(post.languages).length > 0) {
59
+ const langs = Object.keys(post.languages);
60
+ // Add current language to available if not already there
61
+ if (!langs.includes(currentLanguage)) {
62
+ langs.push(currentLanguage);
63
+ }
64
+ setAvailableLanguages(langs);
65
+ }
49
66
  // After loading, ensure we're marked as clean
50
67
  // Use setTimeout to ensure this runs after the reducer has processed LOAD_POST
51
68
  setTimeout(() => {
52
69
  dispatch({ type: 'MARK_CLEAN' });
53
70
  }, 0);
54
71
  },
55
- () => setHeroBlock(null)
72
+ () => setHeroBlock(null),
73
+ !isLoadingLanguage ? currentLanguage : undefined
56
74
  );
57
75
 
76
+ // Sync currentLanguage to editor state on mount so onSave always knows the active language
77
+ useEffect(() => {
78
+ dispatch({ type: 'SET_CURRENT_LANGUAGE', payload: currentLanguage });
79
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount
80
+
81
+ // Fetch primary language from settings (simulated - in real app would come from config)
82
+ useEffect(() => {
83
+ const fetchLanguageSettings = async () => {
84
+ try {
85
+ // Use the locale prop as the initial language
86
+ // Only set this once on initial load, don't override if user already selected a language
87
+ if (!currentLanguage || currentLanguage === 'en') {
88
+ setPrimaryLanguage(locale || 'nl');
89
+ if (!availableLanguages.includes(locale || 'nl')) {
90
+ setAvailableLanguages([locale || 'nl']);
91
+ }
92
+ setCurrentLanguage(locale || 'nl');
93
+ dispatch({ type: 'SET_CURRENT_LANGUAGE', payload: locale || 'nl' });
94
+ }
95
+ } catch (error) {
96
+ console.error('Failed to fetch language settings:', error);
97
+ } finally {
98
+ setIsLoadingLanguage(false);
99
+ }
100
+ };
101
+ fetchLanguageSettings();
102
+ }, []); // Empty dependency - only run once on mount
103
+
104
+ // Handle language change
105
+ const handleLanguageChange = async (newLanguage: string) => {
106
+ // Save current content first if dirty
107
+ if (state.isDirty) {
108
+ const confirmed = window.confirm('You have unsaved changes. Do you want to save them first?');
109
+ if (confirmed) {
110
+ await handleSave();
111
+ }
112
+ }
113
+
114
+ setCurrentLanguage(newLanguage);
115
+ dispatch({ type: 'SET_CURRENT_LANGUAGE', payload: newLanguage });
116
+
117
+ // Reload with new language
118
+ if (postId) {
119
+ try {
120
+ const response = await fetch(`/api/plugin-blog/${postId}?language=${newLanguage}`);
121
+ if (response && response.ok) {
122
+ const apiDoc = await response.json();
123
+
124
+ // Manually update state instead of going through loadPost
125
+ // This avoids re-triggering the usePostLoader hook
126
+ const blocks = apiDoc.contentBlocks || apiDoc.blocks || [];
127
+ dispatch({ type: 'SET_TITLE', payload: apiDoc.title || '' });
128
+
129
+ // Replace all blocks
130
+ // First clear, then set new blocks via LOAD_POST-like behavior
131
+ // We use a custom dispatch to update blocks without resetting postId
132
+ dispatch({
133
+ type: 'LOAD_POST',
134
+ payload: {
135
+ id: state.postId || apiDoc._id || apiDoc.id,
136
+ title: apiDoc.title || '',
137
+ slug: apiDoc.slug || state.slug,
138
+ blocks: blocks,
139
+ seo: apiDoc.seo || {},
140
+ publication: {
141
+ status: apiDoc.publicationData?.status === 'concept' ? 'draft' : (apiDoc.publicationData?.status || state.status),
142
+ date: apiDoc.publicationData?.date,
143
+ authorId: apiDoc.authorId,
144
+ },
145
+ metadata: {
146
+ featuredImage: apiDoc.image ? {
147
+ id: apiDoc.image.id || apiDoc.image.src,
148
+ alt: apiDoc.image.alt,
149
+ isCustom: apiDoc.image.isCustom,
150
+ } : state.metadata?.featuredImage,
151
+ categories: apiDoc.categoryTags?.category ? [apiDoc.categoryTags.category] : [],
152
+ tags: apiDoc.categoryTags?.tags || [],
153
+ excerpt: apiDoc.summary || '',
154
+ lang: newLanguage,
155
+ },
156
+ languages: apiDoc.languages,
157
+ createdAt: apiDoc.createdAt || new Date().toISOString(),
158
+ updatedAt: apiDoc.updatedAt || new Date().toISOString(),
159
+ }
160
+ });
161
+
162
+ // Update available languages
163
+ if (apiDoc.availableLanguages) {
164
+ const langs = [...apiDoc.availableLanguages];
165
+ if (!langs.includes(newLanguage)) {
166
+ langs.push(newLanguage);
167
+ }
168
+ setAvailableLanguages(langs);
169
+ }
170
+
171
+ // Reset hero block so it re-initializes from new blocks
172
+ setHeroBlock(null);
173
+
174
+ setTimeout(() => {
175
+ dispatch({ type: 'MARK_CLEAN' });
176
+ }, 100);
177
+ }
178
+ } catch (error) {
179
+ console.error('Failed to switch language:', error);
180
+ }
181
+ }
182
+ };
183
+
184
+ // Handle adding a new language
185
+ const handleAddLanguage = async (newLanguage: string) => {
186
+ if (availableLanguages.includes(newLanguage)) return;
187
+
188
+ // Add the new language to the list
189
+ setAvailableLanguages([...availableLanguages, newLanguage]);
190
+
191
+ // Switch to the new language (it will copy from primary language)
192
+ await handleLanguageChange(newLanguage);
193
+ };
194
+
58
195
  // Track if we just loaded a post to prevent marking as dirty during cleanup
59
196
  const justLoadedRef = useRef(false);
60
197
  const previousIsLoadingRef = useRef<boolean>(false);
61
198
  const loadingCleanupTimerRef = useRef<NodeJS.Timeout | null>(null);
62
-
199
+
63
200
  // Mark when post loading completes and ensure it stays clean after all effects
64
201
  useEffect(() => {
65
202
  // Detect when loading just finished (was loading, now not loading, and we have a postId)
66
203
  const loadingJustFinished = previousIsLoadingRef.current && !isLoadingPost && state.postId;
67
-
204
+
68
205
  if (loadingJustFinished) {
69
206
  justLoadedRef.current = true;
70
-
207
+
71
208
  // Clear any existing cleanup timer
72
209
  if (loadingCleanupTimerRef.current) {
73
210
  clearTimeout(loadingCleanupTimerRef.current);
74
211
  }
75
-
212
+
76
213
  // Wait for all effects to complete, then ensure we're marked as clean
77
214
  // Use multiple animation frames + setTimeout to ensure all effects have run
78
215
  requestAnimationFrame(() => {
@@ -87,10 +224,10 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
87
224
  });
88
225
  });
89
226
  }
90
-
227
+
91
228
  // Update ref
92
229
  previousIsLoadingRef.current = isLoadingPost;
93
-
230
+
94
231
  return () => {
95
232
  if (loadingCleanupTimerRef.current) {
96
233
  clearTimeout(loadingCleanupTimerRef.current);
@@ -170,7 +307,7 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
170
307
  }
171
308
 
172
309
  console.log('[CanvasEditorView] Final status before save:', state.status);
173
-
310
+
174
311
  // Pass hero block to save function so it can be included in the saved data
175
312
  await helpers.save(heroBlock);
176
313
  setIsSaving(false);
@@ -199,17 +336,17 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
199
336
  // Handle hero block update
200
337
  const handleHeroBlockUpdate = (data: Partial<Block['data']>) => {
201
338
  if (!heroBlock) return;
202
-
339
+
203
340
  setHeroBlock({
204
341
  ...heroBlock,
205
342
  data: { ...heroBlock.data, ...data },
206
343
  });
207
-
344
+
208
345
  // Sync title to editor state
209
346
  if (data.title !== undefined && typeof data.title === 'string') {
210
347
  dispatch({ type: 'SET_TITLE', payload: data.title });
211
348
  }
212
-
349
+
213
350
  // Sync summary to editor state metadata
214
351
  if (data.summary !== undefined && typeof data.summary === 'string') {
215
352
  dispatch({
@@ -217,11 +354,11 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
217
354
  payload: { excerpt: data.summary }
218
355
  });
219
356
  }
220
-
357
+
221
358
  // Hero image and featured image are completely independent
222
359
  // Do NOT sync hero image to featured image
223
360
  // The featured image is a separate thumbnail that the client adjusts independently
224
-
361
+
225
362
  // Sync category to editor state metadata
226
363
  if (data.category !== undefined && typeof data.category === 'string') {
227
364
  dispatch({
@@ -244,11 +381,9 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
244
381
  };
245
382
 
246
383
  return (
247
- <div className="h-full rounded-[2.5rem] w-full bg-dashboard-card text-dashboard-text flex flex-col font-sans transition-colors duration-300 overflow-hidden relative">
248
- <main className="flex flex-1 flex-col relative min-h-0">
249
- {/* Error Banner */}
250
- <ErrorBanner error={saveError} onDismiss={() => setSaveError(null)} />
251
-
384
+ <div className="h-full rounded-[2.5rem] w-full bg-dashboard-card text-dashboard-text flex flex-col font-sans transition-colors duration-300 overflow-hidden">
385
+ {/* Header needs overflow-visible for dropdown to escape the rounded container */}
386
+ <header className="overflow-visible flex-none shrink-0 z-10">
252
387
  <EditorHeader
253
388
  isLibraryOpen={isLibraryOpen}
254
389
  onLibraryToggle={() => setLibraryOpen(!isLibraryOpen)}
@@ -275,9 +410,18 @@ export function CanvasEditorView({ postId, darkMode, backgroundColors: propsBack
275
410
  isDirty={state.isDirty}
276
411
  autoSaveCountdown={countdown}
277
412
  autoSaveStatus={saveStatus}
413
+ languages={availableLanguages}
414
+ currentLanguage={currentLanguage}
415
+ onLanguageChange={handleLanguageChange}
416
+ onAddLanguage={handleAddLanguage}
278
417
  />
418
+ </header>
419
+
420
+ {/* Error Banner */}
421
+ <ErrorBanner error={saveError} onDismiss={() => setSaveError(null)} />
279
422
 
280
- {/* Editor Content Wrapper */}
423
+ {/* Editor Content Wrapper */}
424
+ <main className="flex flex-1 flex-col relative min-h-0 overflow-hidden">
281
425
  <div className="flex flex-1 relative overflow-hidden min-h-0 flex-nowrap">
282
426
  {/* LEFT SIDEBAR: COMPONENT LIBRARY */}
283
427
  {!isPreviewMode && (
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState } from 'react';
4
- import { ArrowLeft, Library, Settings2, Save, Clock, Edit, Eye } from 'lucide-react';
4
+ import { ArrowLeft, Library, Settings2, Save, Clock, Edit, Eye, Globe, Plus, ChevronDown } from 'lucide-react';
5
5
  import { useEditor } from '../../state/EditorContext';
6
6
  import { SaveConfirmationModal } from './SaveConfirmationModal';
7
7
 
@@ -20,6 +20,10 @@ export interface EditorHeaderProps {
20
20
  isDirty?: boolean;
21
21
  autoSaveCountdown?: number | null;
22
22
  autoSaveStatus?: 'idle' | 'saving' | 'saved' | 'error';
23
+ languages?: string[];
24
+ currentLanguage?: string;
25
+ onLanguageChange?: (language: string) => void;
26
+ onAddLanguage?: (language: string) => void;
23
27
  }
24
28
 
25
29
  export function EditorHeader({
@@ -37,11 +41,27 @@ export function EditorHeader({
37
41
  isDirty = false,
38
42
  autoSaveCountdown = null,
39
43
  autoSaveStatus = 'idle',
44
+ languages = [],
45
+ currentLanguage = 'en',
46
+ onLanguageChange,
47
+ onAddLanguage,
40
48
  }: EditorHeaderProps) {
41
49
  const { state, dispatch } = useEditor();
42
50
  const [showConfirmModal, setShowConfirmModal] = useState(false);
43
51
  const [saveAsDraft, setSaveAsDraft] = useState(false);
44
52
  const [saveError, setSaveError] = useState<string | null>(null);
53
+ const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
54
+
55
+ const languageLabels: Record<string, string> = {
56
+ en: 'English',
57
+ nl: 'Nederlands',
58
+ sv: 'Svenska',
59
+ de: 'Deutsch',
60
+ fr: 'Français',
61
+ es: 'Español',
62
+ };
63
+
64
+ const availableToAdd = ['en', 'nl', 'sv', 'de', 'fr', 'es'].filter(lang => !languages.includes(lang));
45
65
 
46
66
  const handleSaveDraftClick = () => {
47
67
  setSaveAsDraft(true);
@@ -104,7 +124,7 @@ export function EditorHeader({
104
124
  };
105
125
 
106
126
  return (
107
- <header className="flex items-center justify-between px-6 py-3 bg-dashboard-sidebar backdrop-blur-md border-b border-dashboard-border flex-none shrink-0">
127
+ <div className="flex items-center justify-between px-6 py-3 bg-dashboard-sidebar backdrop-blur-md border-b border-dashboard-border flex-none shrink-0">
108
128
  <div className="flex items-center gap-6">
109
129
  <button
110
130
  onClick={() => {
@@ -131,6 +151,64 @@ export function EditorHeader({
131
151
  <Library size={16} strokeWidth={1.5} />
132
152
  Library
133
153
  </button>
154
+ {/* Language Switcher */}
155
+ {languages.length > 0 && onLanguageChange && (
156
+ <>
157
+ <div className="h-4 w-[1px] bg-neutral-300 dark:border-neutral-700" />
158
+ <div className="relative">
159
+ <button
160
+ onClick={() => setShowLanguageDropdown(!showLanguageDropdown)}
161
+ className="flex items-center gap-2 px-3 py-2 text-[10px] uppercase tracking-widest font-bold bg-dashboard-card border border-dashboard-border rounded-lg text-neutral-600 dark:text-neutral-400 hover:text-dashboard-text hover:border-primary/50 transition-all shadow-sm"
162
+ >
163
+ <Globe size={14} strokeWidth={1.5} />
164
+ <span>{languageLabels[currentLanguage] || currentLanguage.toUpperCase()}</span>
165
+ <ChevronDown size={12} className={`transition-transform ${showLanguageDropdown ? 'rotate-180' : ''}`} />
166
+ </button>
167
+ {showLanguageDropdown && (
168
+ <div className="absolute top-full left-0 mt-2 py-1 bg-dashboard-card border border-dashboard-border rounded-lg shadow-xl z-[9999] min-w-[160px] overflow-hidden">
169
+ <div className="px-3 py-2 text-[9px] text-neutral-500 uppercase tracking-wider border-b border-dashboard-border">
170
+ Beschikbare Talen
171
+ </div>
172
+ {languages.map(lang => (
173
+ <button
174
+ key={lang}
175
+ onClick={() => {
176
+ onLanguageChange(lang);
177
+ setShowLanguageDropdown(false);
178
+ }}
179
+ className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-wider transition-colors ${lang === currentLanguage
180
+ ? 'bg-primary/10 text-primary font-bold'
181
+ : 'text-neutral-600 dark:text-neutral-400 hover:bg-dashboard-bg hover:text-dashboard-text'
182
+ }`}
183
+ >
184
+ {languageLabels[lang] || lang.toUpperCase()}
185
+ </button>
186
+ ))}
187
+ {availableToAdd.length > 0 && onAddLanguage && (
188
+ <>
189
+ <div className="my-1 border-t border-dashboard-border" />
190
+ <div className="px-3 py-1 text-[9px] text-neutral-500 uppercase tracking-wider">
191
+ Add Language
192
+ </div>
193
+ {availableToAdd.slice(0, 3).map(lang => (
194
+ <button
195
+ key={lang}
196
+ onClick={() => {
197
+ onAddLanguage(lang);
198
+ setShowLanguageDropdown(false);
199
+ }}
200
+ className="w-full text-left px-3 py-2 text-[10px] uppercase tracking-wider text-neutral-600 dark:text-neutral-400 hover:bg-dashboard-bg hover:text-dashboard-text transition-colors"
201
+ >
202
+ + {languageLabels[lang] || lang.toUpperCase()}
203
+ </button>
204
+ ))}
205
+ </>
206
+ )}
207
+ </div>
208
+ )}
209
+ </div>
210
+ </>
211
+ )}
134
212
  </div>
135
213
 
136
214
  <div className="flex items-center gap-4">
@@ -139,11 +217,10 @@ export function EditorHeader({
139
217
  <div className="flex items-center gap-2">
140
218
  <button
141
219
  onClick={() => onAutoSaveToggle(!autoSaveEnabled)}
142
- className={`relative flex items-center gap-2 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
143
- autoSaveEnabled
144
- ? 'bg-primary/20 text-primary border border-primary/30'
145
- : 'bg-dashboard-bg text-neutral-600 dark:text-neutral-400 border border-dashboard-border hover:text-neutral-950 dark:hover:text-white'
146
- }`}
220
+ className={`relative flex items-center gap-2 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${autoSaveEnabled
221
+ ? 'bg-primary/20 text-primary border border-primary/30'
222
+ : 'bg-dashboard-bg text-neutral-600 dark:text-neutral-400 border border-dashboard-border hover:text-neutral-950 dark:hover:text-white'
223
+ }`}
147
224
  title={autoSaveEnabled ? 'Auto-save enabled (saves after 10s of inactivity)' : 'Click to enable auto-save'}
148
225
  >
149
226
  <Clock size={12} className={autoSaveEnabled && autoSaveStatus !== 'saving' ? 'animate-pulse' : ''} />
@@ -185,11 +262,10 @@ export function EditorHeader({
185
262
  onPreviewToggle();
186
263
  }
187
264
  }}
188
- className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
189
- !isPreviewMode
190
- ? 'bg-primary text-white shadow-sm'
191
- : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
192
- }`}
265
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${!isPreviewMode
266
+ ? 'bg-primary text-white shadow-sm'
267
+ : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
268
+ }`}
193
269
  title="Edit mode - Make changes to your post"
194
270
  >
195
271
  <Edit size={12} strokeWidth={2.5} />
@@ -201,11 +277,10 @@ export function EditorHeader({
201
277
  onPreviewToggle();
202
278
  }
203
279
  }}
204
- className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${
205
- isPreviewMode
206
- ? 'bg-primary text-white shadow-sm'
207
- : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
208
- }`}
280
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] uppercase tracking-widest font-bold transition-all ${isPreviewMode
281
+ ? 'bg-primary text-white shadow-sm'
282
+ : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
283
+ }`}
209
284
  title="Preview mode - See how your post will look"
210
285
  >
211
286
  <Eye size={12} strokeWidth={2.5} />
@@ -262,7 +337,7 @@ export function EditorHeader({
262
337
  saveAsDraft={saveAsDraft}
263
338
  error={saveError}
264
339
  />
265
- </header>
340
+ </div>
266
341
  );
267
342
  }
268
343
 
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useState, useRef } from 'react';
2
2
  import { apiToBlogPost, type APIBlogDocument } from '../../../lib/mappers/apiMapper';
3
3
  import type { BlogPost } from '../../../types/post';
4
4
 
@@ -6,9 +6,13 @@ export function usePostLoader(
6
6
  postId: string | undefined,
7
7
  currentPostId: string | null,
8
8
  loadPost: (post: BlogPost) => void,
9
- resetHeroBlock: () => void
9
+ resetHeroBlock: () => void,
10
+ language?: string
10
11
  ) {
11
12
  const [isLoadingPost, setIsLoadingPost] = useState(false);
13
+ // Use a ref to track language so changes don't re-trigger initial load
14
+ const languageRef = useRef(language);
15
+ languageRef.current = language;
12
16
 
13
17
  useEffect(() => {
14
18
  if (postId && !currentPostId) {
@@ -17,7 +21,11 @@ export function usePostLoader(
17
21
  setIsLoadingPost(true);
18
22
  // Reset hero block before loading new post so it gets re-initialized from the new post's blocks
19
23
  resetHeroBlock();
20
- const response = await fetch(`/api/plugin-blog/${postId}`);
24
+ const lang = languageRef.current;
25
+ const url = lang
26
+ ? `/api/plugin-blog/${postId}?language=${lang}`
27
+ : `/api/plugin-blog/${postId}`;
28
+ const response = await fetch(url);
21
29
  if (!response.ok) {
22
30
  throw new Error('Failed to load post');
23
31
  }
@@ -33,7 +41,10 @@ export function usePostLoader(
33
41
  };
34
42
  loadPostData();
35
43
  }
36
- }, [postId, currentPostId, loadPost, resetHeroBlock]);
44
+ // Only re-run on initial load (postId / currentPostId change), NOT on language change
45
+ // Language switching is handled separately by handleLanguageChange
46
+ // eslint-disable-next-line react-hooks/exhaustive-deps
47
+ }, [postId, currentPostId]);
37
48
 
38
49
  return { isLoadingPost };
39
50
  }