@jhits/plugin-blog 0.0.17 → 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 (66) 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 +3 -3
  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
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Language Flags Component
3
+ * Displays interactive language flags with custom tooltips
4
+ */
5
+ 'use client';
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ import { useState, useRef, useEffect } from 'react';
8
+ import { createPortal } from 'react-dom';
9
+ import { motion, AnimatePresence } from 'framer-motion';
10
+ const LANGUAGE_COUNTRY_CODES = {
11
+ en: 'gb',
12
+ nl: 'nl',
13
+ sv: 'se',
14
+ de: 'de',
15
+ fr: 'fr',
16
+ es: 'es',
17
+ it: 'it',
18
+ pt: 'pt',
19
+ pl: 'pl',
20
+ ru: 'ru',
21
+ ja: 'jp',
22
+ zh: 'cn',
23
+ ar: 'sa',
24
+ tr: 'tr',
25
+ };
26
+ const getFlagUrl = (lang) => {
27
+ const countryCode = LANGUAGE_COUNTRY_CODES[lang] || lang;
28
+ return `https://flagcdn.com/w40/${countryCode}.png`;
29
+ };
30
+ export function LanguageFlags({ post }) {
31
+ const [hoveredLang, setHoveredLang] = useState(null);
32
+ const [tooltipCoords, setTooltipCoords] = useState(null);
33
+ const flagRefs = useRef({});
34
+ useEffect(() => {
35
+ if (hoveredLang && flagRefs.current[hoveredLang]) {
36
+ const rect = flagRefs.current[hoveredLang].getBoundingClientRect();
37
+ setTooltipCoords({
38
+ top: rect.top, // Viewport-relative
39
+ left: rect.left + rect.width / 2, // Center horizontally
40
+ });
41
+ }
42
+ }, [hoveredLang]);
43
+ if (!post.availableLanguages || post.availableLanguages.length === 0) {
44
+ return null;
45
+ }
46
+ return (_jsx("div", { className: "flex gap-2", children: post.availableLanguages.map((lang) => {
47
+ const langData = post.languages?.[lang];
48
+ const status = langData?.status || 'draft';
49
+ const langTitle = langData?.metadata?.title || post.title;
50
+ const isHovered = hoveredLang === lang;
51
+ return (_jsxs("div", { ref: (el) => { flagRefs.current[lang] = el; }, className: "relative", onMouseEnter: () => setHoveredLang(lang), onMouseLeave: () => setHoveredLang(null), children: [_jsxs(motion.div, { whileHover: { scale: 1.15 }, className: `relative flex items-center justify-center w-7 h-4.5 rounded shadow-sm overflow-hidden border cursor-help transition-colors ${status === 'published'
52
+ ? 'border-green-500/40 bg-white'
53
+ : 'border-amber-500/40 bg-white opacity-90'}`, children: [_jsx("img", { src: getFlagUrl(lang), alt: lang, className: "w-full h-full object-cover" }), status !== 'published' && (_jsx("div", { className: "absolute inset-0 bg-amber-500/5" }))] }), typeof document !== 'undefined' && createPortal(_jsx(AnimatePresence, { children: isHovered && tooltipCoords && (_jsx(motion.div, { initial: { opacity: 0, y: -90, x: '-50%', scale: 0.95 }, animate: { opacity: 1, y: -100, x: '-50%', scale: 1 }, exit: { opacity: 0, y: -90, x: '-50%', scale: 0.95 }, className: "fixed z-[9999] pointer-events-none", style: {
54
+ top: tooltipCoords.top - 12,
55
+ left: tooltipCoords.left,
56
+ }, children: _jsxs("div", { className: "bg-neutral-900 dark:bg-neutral-800 text-white p-3 rounded-xl shadow-2xl border border-neutral-700/50 backdrop-blur-md min-w-[180px] max-w-[280px]", children: [_jsxs("div", { className: "flex items-center justify-between gap-4 mb-1.5", children: [_jsxs("span", { className: "text-[10px] font-black uppercase tracking-widest text-neutral-400", children: [lang.toUpperCase(), " Version"] }), _jsx("span", { className: `text-[9px] font-black uppercase px-1.5 py-0.5 rounded ${status === 'published'
57
+ ? 'bg-green-500/20 text-green-400'
58
+ : 'bg-amber-500/20 text-amber-400'}`, children: status })] }), _jsx("p", { className: "text-xs font-bold leading-snug line-clamp-2", children: langTitle }), _jsx("div", { className: "absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-neutral-900 dark:border-t-neutral-800" })] }) })) }), document.body)] }, lang));
59
+ }) }));
60
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"PostCards.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostCards.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAI5D,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AA0BD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,MAAM,EACN,MAAM,EACN,SAAS,EACT,WAAW,EACX,QAAQ,GACX,EAAE,cAAc,2CA6IhB"}
1
+ {"version":3,"file":"PostCards.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostCards.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAK5D,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AA4BD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,MAAM,EACN,MAAM,EACN,SAAS,EACT,WAAW,EACX,QAAQ,GACX,EAAE,cAAc,2CAoJhB"}
@@ -8,6 +8,7 @@ import { useState, useEffect } from 'react';
8
8
  import { Calendar, User, UserCheck } from 'lucide-react';
9
9
  import { Image } from '@jhits/plugin-images';
10
10
  import { PostActionsMenu } from './PostActionsMenu';
11
+ import { LanguageFlags } from './LanguageFlags';
11
12
  import { useSession } from 'next-auth/react';
12
13
  function getStatusBadgeColor(status) {
13
14
  switch (status) {
@@ -19,6 +20,8 @@ function getStatusBadgeColor(status) {
19
20
  return 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20';
20
21
  case 'archived':
21
22
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
23
+ case 'not-translated':
24
+ return 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20 italic';
22
25
  default:
23
26
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
24
27
  }
@@ -73,5 +76,5 @@ export function PostCards({ posts, locale, onEdit, onPreview, onDuplicate, onDel
73
76
  return 'Unknown';
74
77
  return userMap[authorId] || authorId;
75
78
  };
76
- return (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6", children: posts.map((post) => (_jsxs("div", { className: "bg-dashboard-card rounded-2xl border border-dashboard-border overflow-hidden hover:shadow-xl transition-all duration-300 group", children: [_jsxs("div", { className: "relative w-full h-48 bg-neutral-200 dark:bg-neutral-800 overflow-hidden", children: [post.featuredImage ? (_jsx(Image, { id: post.featuredImage, alt: post.title, fill: true, editable: false, className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center", children: _jsx("span", { className: "text-sm text-neutral-400", children: "No Image" }) })), isPostOwner(post) && (_jsx("div", { className: "absolute top-4 left-4 z-10", children: _jsx("div", { className: "bg-white/90 dark:bg-neutral-900/90 backdrop-blur-sm rounded-full p-1 shadow-lg border border-neutral-200 dark:border-neutral-700", children: _jsx(PostActionsMenu, { onEdit: () => onEdit(post.id), onPreview: () => onPreview(post.id), onDuplicate: () => onDuplicate(post.id), onDelete: () => onDelete(post.id) }) }) })), _jsx("div", { className: "absolute top-4 right-4", children: _jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider border backdrop-blur-sm ${getStatusBadgeColor(post.status)}`, children: post.status }) })] }), _jsxs("div", { className: "p-6", children: [_jsxs("div", { className: "mb-4", children: [_jsx("button", { onClick: () => onEdit(post.id), className: "text-left w-full hover:cursor-pointer", children: _jsx("h3", { className: "font-bold text-lg text-neutral-950 dark:text-white mb-2 line-clamp-2 group-hover:text-primary transition-colors hover:underline", children: post.title }) }), _jsxs("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 font-mono", children: ["/", post.slug] })] }), post.excerpt && (_jsx("p", { className: "text-sm text-neutral-600 dark:text-neutral-400 mb-4 line-clamp-2", children: post.excerpt })), _jsxs("div", { className: "space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700", children: [_jsxs("div", { className: "flex items-center gap-2", children: [isPostOwner(post) ? (_jsx(UserCheck, { size: 14, className: "text-primary" })) : (_jsx(User, { size: 14, className: "text-neutral-400" })), _jsxs("span", { className: `text-xs ${isPostOwner(post) ? 'text-primary font-semibold' : 'text-neutral-600 dark:text-neutral-400'}`, children: [getAuthorName(post.authorId), isPostOwner(post) && (_jsx("span", { className: "ml-1", children: "(You)" }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 14, className: "text-neutral-400" }), _jsx("span", { className: "text-xs text-neutral-600 dark:text-neutral-400", children: formatDate(post.updatedAt, locale) })] })] })] })] }, post.id))) }));
79
+ return (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6", children: posts.map((post) => (_jsxs("div", { className: "flex flex-col bg-dashboard-card rounded-2xl border border-dashboard-border overflow-hidden hover:shadow-xl transition-all duration-300 group h-full", children: [_jsxs("div", { className: "relative w-full h-48 bg-neutral-200 dark:bg-neutral-800 overflow-hidden flex-shrink-0", children: [post.featuredImage ? (_jsx(Image, { id: post.featuredImage, alt: post.title, fill: true, editable: false, className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center", children: _jsx("span", { className: "text-sm text-neutral-400", children: "No Image" }) })), isPostOwner(post) && (_jsx("div", { className: "absolute top-4 left-4 z-10", children: _jsx("div", { className: "bg-white/90 dark:bg-neutral-900/90 backdrop-blur-sm rounded-full p-1 shadow-lg border border-neutral-200 dark:border-neutral-700", children: _jsx(PostActionsMenu, { onEdit: () => onEdit(post.id), onPreview: () => onPreview(post.id), onDuplicate: () => onDuplicate(post.id), onDelete: () => onDelete(post.id) }) }) })), _jsx("div", { className: "absolute top-4 right-4", children: _jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider border backdrop-blur-sm ${getStatusBadgeColor(post.status)}`, children: post.status }) })] }), _jsxs("div", { className: "p-6 flex flex-col flex-1", children: [_jsxs("div", { className: "mb-4 min-h-[72px]", children: [_jsx("button", { onClick: () => onEdit(post.id), className: "text-left w-full hover:cursor-pointer", children: _jsx("h3", { className: "font-bold text-lg text-neutral-950 dark:text-white mb-2 line-clamp-2 group-hover:text-primary transition-colors hover:underline", children: post.title }) }), _jsxs("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 font-mono line-clamp-1", children: ["/", post.slug] })] }), _jsx("div", { className: "flex-1 mb-4 min-h-[40px]", children: post.excerpt && (_jsx("p", { className: "text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2", children: post.excerpt })) }), _jsx("div", { className: "mb-6", children: _jsx(LanguageFlags, { post: post }) }), _jsxs("div", { className: "space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700 mt-auto", children: [_jsxs("div", { className: "flex items-center gap-2", children: [isPostOwner(post) ? (_jsx(UserCheck, { size: 14, className: "text-primary" })) : (_jsx(User, { size: 14, className: "text-neutral-400" })), _jsxs("span", { className: `text-xs ${isPostOwner(post) ? 'text-primary font-semibold' : 'text-neutral-600 dark:text-neutral-400'}`, children: [getAuthorName(post.authorId), isPostOwner(post) && (_jsx("span", { className: "ml-1", children: "(You)" }))] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 14, className: "text-neutral-400" }), _jsx("span", { className: "text-xs text-neutral-600 dark:text-neutral-400", children: formatDate(post.updatedAt, locale) })] })] })] })] }, post.id))) }));
77
80
  }
@@ -11,6 +11,9 @@ export interface PostFiltersProps {
11
11
  categoryFilter: string;
12
12
  onCategoryFilterChange: (value: string) => void;
13
13
  categories: string[];
14
+ language: string;
15
+ onLanguageChange: (value: string) => void;
16
+ availableLanguages: string[];
14
17
  }
15
- export declare function PostFilters({ search, onSearchChange, statusFilter, onStatusFilterChange, categoryFilter, onCategoryFilterChange, categories, }: PostFiltersProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function PostFilters({ search, onSearchChange, statusFilter, onStatusFilterChange, categoryFilter, onCategoryFilterChange, categories, language, onLanguageChange, availableLanguages, }: PostFiltersProps): import("react/jsx-runtime").JSX.Element;
16
19
  //# sourceMappingURL=PostFilters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PostFilters.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostFilters.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,UAAU,GAAG,KAAK,CAAC;IACjC,oBAAoB,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,KAAK,IAAI,CAAC;IAC1D,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,sBAAsB,EACtB,UAAU,GACb,EAAE,gBAAgB,2CAgElB"}
1
+ {"version":3,"file":"PostFilters.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostFilters.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,UAAU,GAAG,KAAK,CAAC;IACjC,oBAAoB,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,KAAK,IAAI,CAAC;IAC1D,cAAc,EAAE,MAAM,CAAC;IACvB,sBAAsB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,sBAAsB,EACtB,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,kBAAkB,GACrB,EAAE,gBAAgB,2CA+FlB"}
@@ -4,7 +4,17 @@
4
4
  */
5
5
  'use client';
6
6
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
- import { Search, Filter, Tag } from 'lucide-react';
8
- export function PostFilters({ search, onSearchChange, statusFilter, onStatusFilterChange, categoryFilter, onCategoryFilterChange, categories, }) {
9
- return (_jsxs("div", { className: "flex flex-col sm:flex-row gap-4 mb-6", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx("label", { htmlFor: "blog-post-search", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Search posts by title or content" }), _jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4" }), _jsx("input", { id: "blog-post-search", name: "blog-post-search", type: "text", value: search, onChange: (e) => onSearchChange(e.target.value), placeholder: "Search posts by title or content...", className: "w-full pl-11 pr-4 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "blog-post-status-filter", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Filter by status" }), _jsx(Filter, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" }), _jsxs("select", { id: "blog-post-status-filter", name: "blog-post-status-filter", value: statusFilter, onChange: (e) => onStatusFilterChange(e.target.value), className: "pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]", children: [_jsx("option", { value: "all", children: "All Statuses" }), _jsx("option", { value: "published", children: "Published" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "archived", children: "Archived" })] })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "blog-post-category-filter", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Filter by category" }), _jsx(Tag, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" }), _jsxs("select", { id: "blog-post-category-filter", name: "blog-post-category-filter", value: categoryFilter, onChange: (e) => onCategoryFilterChange(e.target.value), className: "pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]", children: [_jsx("option", { value: "all", children: "All Categories" }), categories.map((category) => (_jsx("option", { value: category, children: category }, category)))] })] })] }));
7
+ import { Search, Filter, Tag, Globe } from 'lucide-react';
8
+ export function PostFilters({ search, onSearchChange, statusFilter, onStatusFilterChange, categoryFilter, onCategoryFilterChange, categories, language, onLanguageChange, availableLanguages, }) {
9
+ const langNames = {
10
+ nl: 'Nederlands (NL)',
11
+ en: 'English (EN)',
12
+ sv: 'Svenska (SV)',
13
+ de: 'Deutsch (DE)',
14
+ fr: 'Français (FR)',
15
+ es: 'Español (ES)',
16
+ it: 'Italiano (IT)',
17
+ pt: 'Português (PT)',
18
+ };
19
+ return (_jsxs("div", { className: "flex flex-col sm:flex-row gap-4 mb-6", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx("label", { htmlFor: "blog-post-search", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Search posts by title or content" }), _jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4" }), _jsx("input", { id: "blog-post-search", name: "blog-post-search", type: "text", value: search, onChange: (e) => onSearchChange(e.target.value), placeholder: "Search posts by title or content...", className: "w-full pl-11 pr-4 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all" })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "blog-post-status-filter", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Filter by status" }), _jsx(Filter, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" }), _jsxs("select", { id: "blog-post-status-filter", name: "blog-post-status-filter", value: statusFilter, onChange: (e) => onStatusFilterChange(e.target.value), className: "pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]", children: [_jsx("option", { value: "all", children: "All Statuses" }), _jsx("option", { value: "published", children: "Published" }), _jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "scheduled", children: "Scheduled" }), _jsx("option", { value: "archived", children: "Archived" })] })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "blog-post-category-filter", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Filter by category" }), _jsx(Tag, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" }), _jsxs("select", { id: "blog-post-category-filter", name: "blog-post-category-filter", value: categoryFilter, onChange: (e) => onCategoryFilterChange(e.target.value), className: "pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[160px]", children: [_jsx("option", { value: "all", children: "All Categories" }), categories.map((category) => (_jsx("option", { value: category, children: category }, category)))] })] }), _jsxs("div", { className: "relative", children: [_jsx("label", { htmlFor: "blog-post-language-filter", className: "absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", style: { clip: 'rect(0, 0, 0, 0)', clipPath: 'inset(50%)' }, children: "Primary Language" }), _jsx(Globe, { className: "absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 size-4 pointer-events-none" }), _jsx("select", { id: "blog-post-language-filter", name: "blog-post-language-filter", value: language, onChange: (e) => onLanguageChange(e.target.value), className: "pl-11 pr-8 py-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary appearance-none outline-none cursor-pointer min-w-[140px]", children: availableLanguages.map(lang => (_jsx("option", { value: lang, children: langNames[lang] || lang.toUpperCase() }, lang))) })] })] }));
10
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PostManagerView.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostManagerView.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,2CAqQvE"}
1
+ {"version":3,"file":"PostManagerView.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostManagerView.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,2CA4RvE"}
@@ -21,6 +21,7 @@ export function PostManagerView({ siteId, locale }) {
21
21
  const [search, setSearch] = useState('');
22
22
  const [statusFilter, setStatusFilter] = useState('all');
23
23
  const [categoryFilter, setCategoryFilter] = useState('all');
24
+ const [currentLanguage, setCurrentLanguage] = useState(locale || 'nl');
24
25
  // Load view mode preference from localStorage
25
26
  const getStoredViewMode = () => {
26
27
  if (typeof window === 'undefined')
@@ -40,7 +41,7 @@ export function PostManagerView({ siteId, locale }) {
40
41
  const fetchPosts = async () => {
41
42
  try {
42
43
  setLoading(true);
43
- const response = await fetch('/api/plugin-blog?admin=true');
44
+ const response = await fetch(`/api/plugin-blog?admin=true&language=${currentLanguage}`);
44
45
  const data = await response.json();
45
46
  if (data.blogs && Array.isArray(data.blogs)) {
46
47
  // Convert API format to PostListItem format
@@ -75,6 +76,9 @@ export function PostManagerView({ siteId, locale }) {
75
76
  authorId: blogPost.publication.authorId,
76
77
  updatedAt: blogPost.updatedAt,
77
78
  category: category,
79
+ lang: blogPost.metadata.lang,
80
+ availableLanguages: doc.availableLanguages,
81
+ languages: doc.languages,
78
82
  };
79
83
  });
80
84
  setPosts(postListItems);
@@ -89,7 +93,7 @@ export function PostManagerView({ siteId, locale }) {
89
93
  }
90
94
  };
91
95
  fetchPosts();
92
- }, []);
96
+ }, [currentLanguage]);
93
97
  // Extract unique categories from posts
94
98
  const categories = React.useMemo(() => {
95
99
  const categorySet = new Set();
@@ -100,6 +104,23 @@ export function PostManagerView({ siteId, locale }) {
100
104
  });
101
105
  return Array.from(categorySet).sort();
102
106
  }, [posts]);
107
+ // Extract unique languages from all posts
108
+ const availableLanguages = React.useMemo(() => {
109
+ const langSet = new Set();
110
+ posts.forEach(post => {
111
+ if (post.availableLanguages) {
112
+ post.availableLanguages.forEach(lang => langSet.add(lang));
113
+ }
114
+ if (post.lang)
115
+ langSet.add(post.lang);
116
+ });
117
+ // Always include the current locale/language to ensure it's selectable
118
+ if (locale)
119
+ langSet.add(locale);
120
+ if (currentLanguage)
121
+ langSet.add(currentLanguage);
122
+ return Array.from(langSet).sort();
123
+ }, [posts, locale, currentLanguage]);
103
124
  // Filter posts
104
125
  const filteredPosts = React.useMemo(() => {
105
126
  return posts.filter((post) => {
@@ -171,7 +192,7 @@ export function PostManagerView({ siteId, locale }) {
171
192
  }
172
193
  };
173
194
  const hasActiveFilters = search !== '' || statusFilter !== 'all' || categoryFilter !== 'all';
174
- return (_jsxs("div", { className: "h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto", children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-black text-neutral-950 dark:text-white uppercase tracking-tighter mb-2", children: "Blog Posts" }), _jsx("p", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: "Manage your blog posts, drafts, and published content" })] }), _jsxs("button", { onClick: handleCreatePost, className: "inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20", children: [_jsx(Plus, { size: 16 }), "New Post"] })] }), _jsx(PostStats, { total: totalPosts, posts: posts }), _jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6", children: [_jsx(PostFilters, { search: search, onSearchChange: setSearch, statusFilter: statusFilter, onStatusFilterChange: setStatusFilter, categoryFilter: categoryFilter, onCategoryFilterChange: setCategoryFilter, categories: categories }), _jsxs("div", { className: "flex items-center gap-2 bg-neutral-100 dark:bg-neutral-800/50 rounded-full p-1 border border-neutral-300 dark:border-neutral-700", children: [_jsx("button", { onClick: () => setViewMode('list'), className: `p-2 rounded-full transition-all ${viewMode === 'list'
195
+ return (_jsxs("div", { className: "h-full w-full rounded-[2.5rem] bg-white dark:bg-neutral-900 p-8 overflow-y-auto", children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-black text-neutral-950 dark:text-white uppercase tracking-tighter mb-2", children: "Blog Posts" }), _jsx("p", { className: "text-sm text-neutral-500 dark:text-neutral-400", children: "Manage your blog posts, drafts, and published content" })] }), _jsxs("button", { onClick: handleCreatePost, className: "inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20", children: [_jsx(Plus, { size: 16 }), "New Post"] })] }), _jsx(PostStats, { total: totalPosts, posts: posts }), _jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6", children: [_jsx(PostFilters, { search: search, onSearchChange: setSearch, statusFilter: statusFilter, onStatusFilterChange: setStatusFilter, categoryFilter: categoryFilter, onCategoryFilterChange: setCategoryFilter, categories: categories, language: currentLanguage, onLanguageChange: setCurrentLanguage, availableLanguages: availableLanguages }), _jsxs("div", { className: "flex items-center gap-2 bg-neutral-100 dark:bg-neutral-800/50 rounded-full p-1 border border-neutral-300 dark:border-neutral-700", children: [_jsx("button", { onClick: () => setViewMode('list'), className: `p-2 rounded-full transition-all ${viewMode === 'list'
175
196
  ? 'bg-white dark:bg-neutral-900 text-primary shadow-sm'
176
197
  : 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'}`, title: "List View", children: _jsx(List, { size: 18 }) }), _jsx("button", { onClick: () => setViewMode('cards'), className: `p-2 rounded-full transition-all ${viewMode === 'cards'
177
198
  ? 'bg-white dark:bg-neutral-900 text-primary shadow-sm'
@@ -1 +1 @@
1
- {"version":3,"file":"PostTable.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostTable.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAI5D,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AA0BD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,MAAM,EACN,MAAM,EACN,SAAS,EACT,WAAW,EACX,QAAQ,GACX,EAAE,cAAc,2CA8KhB"}
1
+ {"version":3,"file":"PostTable.d.ts","sourceRoot":"","sources":["../../../src/views/PostManager/PostTable.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAK5D,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AA4BD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,MAAM,EACN,MAAM,EACN,SAAS,EACT,WAAW,EACX,QAAQ,GACX,EAAE,cAAc,2CAsLhB"}
@@ -8,6 +8,7 @@ import { useState, useEffect } from 'react';
8
8
  import { Calendar, User, UserCheck } from 'lucide-react';
9
9
  import { Image } from '@jhits/plugin-images';
10
10
  import { PostActionsMenu } from './PostActionsMenu';
11
+ import { LanguageFlags } from './LanguageFlags';
11
12
  import { useSession } from 'next-auth/react';
12
13
  function getStatusBadgeColor(status) {
13
14
  switch (status) {
@@ -19,6 +20,8 @@ function getStatusBadgeColor(status) {
19
20
  return 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20';
20
21
  case 'archived':
21
22
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
23
+ case 'not-translated':
24
+ return 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20 italic';
22
25
  default:
23
26
  return 'bg-neutral-500/10 text-neutral-700 dark:text-neutral-400 border-neutral-500/20';
24
27
  }
@@ -73,5 +76,5 @@ export function PostTable({ posts, locale, onEdit, onPreview, onDuplicate, onDel
73
76
  return 'Unknown';
74
77
  return userMap[authorId] || authorId;
75
78
  };
76
- return (_jsx("div", { className: "bg-neutral-100 dark:bg-neutral-800/50 rounded-[2.5rem] border border-neutral-300 dark:border-neutral-700 overflow-hidden", children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { className: "bg-neutral-200 dark:bg-neutral-900/50 border-b border-neutral-300 dark:border-neutral-700", children: _jsxs("tr", { children: [_jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Post" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Author" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Category" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Status" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Last Modified" }), _jsx("th", { className: "px-6 py-4 text-right text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-neutral-300 dark:divide-neutral-700", children: posts.map((post) => (_jsxs("tr", { className: "hover:bg-white dark:hover:bg-neutral-900/50 transition-colors cursor-pointer", children: [_jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-4", children: [post.featuredImage ? (_jsx("div", { className: "w-16 h-16 rounded-xl bg-neutral-200 dark:bg-neutral-700 overflow-hidden flex-shrink-0 relative", children: _jsx(Image, { id: post.featuredImage, alt: post.title, fill: true, editable: false, className: "w-full h-full object-cover" }) })) : (_jsx("div", { className: "w-16 h-16 rounded-xl bg-neutral-200 dark:bg-neutral-700 flex items-center justify-center flex-shrink-0", children: _jsx("span", { className: "text-xs text-neutral-400", children: "No Image" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("button", { onClick: () => onEdit(post.id), className: "text-left w-full hover:cursor-pointer p-0 m-0 border-0 bg-transparent", children: _jsx("h3", { className: "font-bold hover:underline text-neutral-950 dark:text-white mb-1 line-clamp-1 text-left", children: post.title.trim() }) }), _jsxs("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 font-mono text-left", children: ["/", post.slug] })] })] }) }), _jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [isPostOwner(post) ? (_jsx(UserCheck, { size: 14, className: "text-primary" })) : (_jsx(User, { size: 14, className: "text-neutral-400" })), _jsxs("span", { className: `text-sm ${isPostOwner(post) ? 'text-primary font-semibold' : 'text-neutral-600 dark:text-neutral-400'}`, children: [getAuthorName(post.authorId), isPostOwner(post) && (_jsx("span", { className: "ml-2 text-xs text-primary/70", children: "(Jij)" }))] })] }) }), _jsx("td", { className: "px-6 py-4", children: _jsx("span", { className: "text-sm text-neutral-600 dark:text-neutral-400", children: post.category || 'Uncategorized' }) }), _jsx("td", { className: "px-6 py-4", children: _jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider border ${getStatusBadgeColor(post.status)}`, children: post.status }) }), _jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 14, className: "text-neutral-400" }), _jsx("span", { className: "text-sm text-neutral-600 dark:text-neutral-400", children: formatDate(post.updatedAt, locale) })] }) }), _jsx("td", { className: "px-6 py-4", children: isPostOwner(post) ? (_jsx("div", { className: "flex items-center justify-end", children: _jsx(PostActionsMenu, { onEdit: () => onEdit(post.id), onPreview: () => onPreview(post.id), onDuplicate: () => onDuplicate(post.id), onDelete: () => onDelete(post.id) }) })) : (_jsx("div", { className: "flex items-center justify-end text-neutral-400 text-xs", children: "Alleen auteur" })) })] }, post.id))) })] }) }) }));
79
+ return (_jsx("div", { className: "bg-neutral-100 dark:bg-neutral-800/50 rounded-[2.5rem] border border-neutral-300 dark:border-neutral-700 overflow-hidden", children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { className: "bg-neutral-200 dark:bg-neutral-900/50 border-b border-neutral-300 dark:border-neutral-700", children: _jsxs("tr", { children: [_jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Post" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Author" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Category" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Languages" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Status" }), _jsx("th", { className: "px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Last Modified" }), _jsx("th", { className: "px-6 py-4 text-right text-[10px] font-black uppercase tracking-widest text-neutral-600 dark:text-neutral-400", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-neutral-300 dark:divide-neutral-700", children: posts.map((post) => (_jsxs("tr", { className: "hover:bg-white dark:hover:bg-neutral-900/50 transition-colors cursor-pointer", children: [_jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-4", children: [post.featuredImage ? (_jsx("div", { className: "w-16 h-16 rounded-xl bg-neutral-200 dark:bg-neutral-700 overflow-hidden flex-shrink-0 relative", children: _jsx(Image, { id: post.featuredImage, alt: post.title, fill: true, editable: false, className: "w-full h-full object-cover" }) })) : (_jsx("div", { className: "w-16 h-16 rounded-xl bg-neutral-200 dark:bg-neutral-700 flex items-center justify-center flex-shrink-0", children: _jsx("span", { className: "text-xs text-neutral-400", children: "No Image" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("button", { onClick: () => onEdit(post.id), className: "text-left w-full hover:cursor-pointer p-0 m-0 border-0 bg-transparent", children: _jsx("h3", { className: "font-bold hover:underline text-neutral-950 dark:text-white mb-1 line-clamp-1 text-left", children: post.title.trim() }) }), _jsxs("p", { className: "text-xs text-neutral-500 dark:text-neutral-400 font-mono text-left", children: ["/", post.slug] })] })] }) }), _jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [isPostOwner(post) ? (_jsx(UserCheck, { size: 14, className: "text-primary" })) : (_jsx(User, { size: 14, className: "text-neutral-400" })), _jsxs("span", { className: `text-sm ${isPostOwner(post) ? 'text-primary font-semibold' : 'text-neutral-600 dark:text-neutral-400'}`, children: [getAuthorName(post.authorId), isPostOwner(post) && (_jsx("span", { className: "ml-2 text-xs text-primary/70", children: "(Jij)" }))] })] }) }), _jsx("td", { className: "px-6 py-4", children: _jsx("span", { className: "text-sm text-neutral-600 dark:text-neutral-400", children: post.category || 'Uncategorized' }) }), _jsx("td", { className: "px-6 py-4", children: _jsx(LanguageFlags, { post: post }) }), _jsx("td", { className: "px-6 py-4", children: _jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider border ${getStatusBadgeColor(post.status)}`, children: post.status }) }), _jsx("td", { className: "px-6 py-4", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 14, className: "text-neutral-400" }), _jsx("span", { className: "text-sm text-neutral-600 dark:text-neutral-400", children: formatDate(post.updatedAt, locale) })] }) }), _jsx("td", { className: "px-6 py-4", children: isPostOwner(post) ? (_jsx("div", { className: "flex items-center justify-end", children: _jsx(PostActionsMenu, { onEdit: () => onEdit(post.id), onPreview: () => onPreview(post.id), onDuplicate: () => onDuplicate(post.id), onDelete: () => onDelete(post.id) }) })) : (_jsx("div", { className: "flex items-center justify-end text-neutral-400 text-xs", children: "Alleen auteur" })) })] }, post.id))) })] }) }) }));
77
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhits/plugin-blog",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Professional blog management system for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,8 +31,8 @@
31
31
  "mongodb": "^7.1.0",
32
32
  "next-auth": "^4.24.13",
33
33
  "@jhits/plugin-content": "0.0.15",
34
- "@jhits/plugin-core": "0.0.10",
35
- "@jhits/plugin-images": "0.0.13"
34
+ "@jhits/plugin-images": "0.0.14",
35
+ "@jhits/plugin-core": "0.0.10"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "next": ">=15.0.0",
@@ -8,27 +8,74 @@ import { BlogApiConfig } from './handler';
8
8
 
9
9
  export async function GET(req: NextRequest, config: BlogApiConfig): Promise<NextResponse> {
10
10
  try {
11
+ const url = new URL(req.url);
12
+ const language = url.searchParams.get('language');
13
+ const publishedOnly = url.searchParams.get('published') === 'true';
14
+
11
15
  const dbConnection = await config.getDb();
12
16
  const db = dbConnection.db();
13
17
  const blogs = db.collection(config.collectionName || 'blogs');
14
18
 
15
- // Get all unique categories from blog posts
16
- const categories = await blogs.distinct('categoryTags.category');
19
+ // Build query
20
+ const query: any = {};
21
+
22
+ if (publishedOnly) {
23
+ // If language is specified, check language status, otherwise root status
24
+ if (language) {
25
+ query[`languages.${language}.status`] = 'published';
26
+ } else {
27
+ query['publicationData.status'] = 'published';
28
+ }
29
+ }
30
+
31
+ if (language) {
32
+ query[`languages.${language}`] = { $exists: true };
33
+ }
34
+
35
+ // 1. Get unique categories from language-specific metadata
36
+ let categories: string[] = [];
37
+ if (language) {
38
+ const result = await blogs.distinct(`languages.${language}.metadata.categories`, query);
39
+ categories = result.filter(Boolean);
40
+ } else {
41
+ const result = await blogs.distinct('categoryTags.category', query);
42
+ categories = result.filter(Boolean);
43
+ }
17
44
 
18
- // Also get categories from Hero blocks in contentBlocks
19
- const heroBlocks = await blogs.aggregate([
20
- { $unwind: '$contentBlocks' },
21
- { $match: { 'contentBlocks.type': 'hero' } },
22
- { $project: { category: '$contentBlocks.data.category' } },
45
+ // 2. Get categories from Hero blocks in contentBlocks
46
+ let heroCategories: string[] = [];
47
+ const pipeline: any[] = [];
48
+
49
+ if (Object.keys(query).length > 0) {
50
+ pipeline.push({ $match: query });
51
+ }
52
+
53
+ if (language) {
54
+ pipeline.push(
55
+ { $project: { blocks: `$languages.${language}.blocks` } },
56
+ { $unwind: '$blocks' },
57
+ { $match: { 'blocks.type': 'hero' } },
58
+ { $project: { category: '$blocks.data.category' } }
59
+ );
60
+ } else {
61
+ pipeline.push(
62
+ { $unwind: '$contentBlocks' },
63
+ { $match: { 'contentBlocks.type': 'hero' } },
64
+ { $project: { category: '$contentBlocks.data.category' } }
65
+ );
66
+ }
67
+
68
+ pipeline.push(
23
69
  { $match: { category: { $exists: true, $ne: null, $nin: [''] } } },
24
- { $group: { _id: '$category' } },
25
- ]).toArray();
70
+ { $group: { _id: '$category' } }
71
+ );
26
72
 
27
- const heroCategories = heroBlocks.map((block: any) => block._id);
73
+ const heroBlocks = await blogs.aggregate(pipeline).toArray();
74
+ heroCategories = heroBlocks.map((block: any) => block._id);
28
75
 
29
76
  // Combine and deduplicate
30
77
  const allCategories = Array.from(
31
- new Set([...categories.filter(Boolean), ...heroCategories.filter(Boolean)])
78
+ new Set([...categories, ...heroCategories])
32
79
  ).sort();
33
80
 
34
81
  return NextResponse.json({ categories: allCategories });