create-nextblock 0.0.1

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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1,81 @@
1
+ // app/cms/blocks/editors/TextBlockEditor.tsx
2
+ 'use client';
3
+
4
+ import React, { useId, useState, useRef, useCallback } from 'react';
5
+ import dynamic from 'next/dynamic';
6
+ import MediaPickerDialog from '@/app/cms/media/components/MediaPickerDialog';
7
+ import { Label } from '@nextblock-cms/ui';
8
+ import { BlockEditorProps } from '../components/BlockEditorModal';
9
+
10
+ // Props expected by NotionEditor
11
+ type NotionEditorProps = {
12
+ content: string;
13
+ onChange: (html: string) => void;
14
+ openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
15
+ };
16
+
17
+ // Use the alias that resolves in your repo; if you mapped @nextblock-cms/editor, swap it here.
18
+ const NotionEditor = dynamic<NotionEditorProps>(
19
+ () => import('@nextblock-cms/editor').then((m) => m.NotionEditor),
20
+ { ssr: false }
21
+ );
22
+
23
+ export type TextBlockContent = {
24
+ html_content?: string;
25
+ };
26
+
27
+ export default function TextBlockEditor({
28
+ content,
29
+ onChange,
30
+ }: BlockEditorProps<Partial<TextBlockContent>>) {
31
+ const labelId = useId();
32
+ const [pickerOpen, setPickerOpen] = useState(false);
33
+ const resolverRef = useRef<null | ((v: any) => void)>(null);
34
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
35
+ const openImagePicker = useCallback(() => {
36
+ setPickerOpen(true);
37
+ return new Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>((resolve) => {
38
+ resolverRef.current = resolve;
39
+ });
40
+ }, []);
41
+
42
+ return (
43
+ <div className="h-full flex flex-col">
44
+ <Label htmlFor={labelId} className="sr-only">
45
+ Text Content
46
+ </Label>
47
+
48
+ <div id={labelId} role="group" aria-labelledby={labelId} className="flex-1 min-h-0 flex flex-col">
49
+ <NotionEditor
50
+ content={content?.html_content ?? ''}
51
+ onChange={(html) => onChange({ html_content: html })}
52
+ openImagePicker={openImagePicker}
53
+ />
54
+
55
+ {/* Hidden controlled MediaPickerDialog for image selection */}
56
+ <div className="sr-only" aria-hidden>
57
+ <MediaPickerDialog
58
+ hideTrigger
59
+ open={pickerOpen}
60
+ onOpenChange={setPickerOpen}
61
+ title="Select or Upload Image"
62
+ accept={(m) => !!m.file_type?.startsWith('image/')}
63
+ onSelect={(media) => {
64
+ const src = `${R2_BASE_URL}/${media.object_key}`;
65
+ resolverRef.current?.({
66
+ src,
67
+ alt: media.description || media.file_name || undefined,
68
+ width: media.width ?? null,
69
+ height: media.height ?? null,
70
+ blurDataURL: media.blur_data_url ?? null,
71
+ });
72
+ resolverRef.current = null;
73
+ setPickerOpen(false);
74
+ }}
75
+ />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { Label } from "@nextblock-cms/ui";
5
+ import { Input } from "@nextblock-cms/ui";
6
+ import { Checkbox } from "@nextblock-cms/ui";
7
+ import { generateDefaultContent, VideoEmbedBlockContent } from "@/lib/blocks/blockRegistry";
8
+ import { BlockEditorProps } from '../components/BlockEditorModal';
9
+
10
+ export default function VideoEmbedBlockEditor({ content, onChange }: BlockEditorProps<Partial<VideoEmbedBlockContent>>) {
11
+ // Get default content from registry
12
+ const defaultContent = generateDefaultContent("video_embed") as VideoEmbedBlockContent;
13
+
14
+ const handleChange = (field: keyof VideoEmbedBlockContent, value: unknown) => {
15
+ onChange({
16
+ ...defaultContent,
17
+ ...content,
18
+ [field]: value,
19
+ });
20
+ };
21
+
22
+ return (
23
+ <div className="space-y-4 p-3 border-t mt-2">
24
+ <div>
25
+ <Label htmlFor="video-url">Video URL</Label>
26
+ <Input
27
+ id="video-url"
28
+ type="url"
29
+ value={content.url || ""}
30
+ onChange={(e) => handleChange("url", e.target.value)}
31
+ placeholder="https://www.youtube.com/watch?v=..."
32
+ />
33
+ </div>
34
+
35
+ <div>
36
+ <Label htmlFor="video-title">Title (Optional)</Label>
37
+ <Input
38
+ id="video-title"
39
+ value={content.title || ""}
40
+ onChange={(e) => handleChange("title", e.target.value)}
41
+ placeholder="Video title"
42
+ />
43
+ </div>
44
+
45
+ <div className="flex items-center space-x-2">
46
+ <Checkbox
47
+ id="autoplay"
48
+ checked={content.autoplay || false}
49
+ onCheckedChange={(checked) => handleChange("autoplay", checked)}
50
+ />
51
+ <Label htmlFor="autoplay">Autoplay</Label>
52
+ </div>
53
+
54
+ <div className="flex items-center space-x-2">
55
+ <Checkbox
56
+ id="controls"
57
+ checked={content.controls !== false}
58
+ onCheckedChange={(checked) => handleChange("controls", checked)}
59
+ />
60
+ <Label htmlFor="controls">Show Controls</Label>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ import React from 'react';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ } from '@nextblock-cms/ui';
12
+ import { Button } from '@nextblock-cms/ui';
13
+
14
+ interface ConfirmationModalProps {
15
+ isOpen: boolean;
16
+ onClose: () => void;
17
+ onConfirm: () => void;
18
+ title: string;
19
+ description: string;
20
+ confirmText?: string;
21
+ cancelText?: string;
22
+ }
23
+
24
+ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
25
+ isOpen,
26
+ onClose,
27
+ onConfirm,
28
+ title,
29
+ description,
30
+ confirmText = 'Confirm',
31
+ cancelText = 'Cancel',
32
+ }) => {
33
+ return (
34
+ <Dialog open={isOpen} onOpenChange={onClose}>
35
+ <DialogContent>
36
+ <DialogHeader>
37
+ <DialogTitle>{title}</DialogTitle>
38
+ <DialogDescription>{description}</DialogDescription>
39
+ </DialogHeader>
40
+ <DialogFooter>
41
+ <Button variant="outline" onClick={onClose}>
42
+ {cancelText}
43
+ </Button>
44
+ <Button onClick={onConfirm}>
45
+ {confirmText}
46
+ </Button>
47
+ </DialogFooter>
48
+ </DialogContent>
49
+ </Dialog>
50
+ );
51
+ };
@@ -0,0 +1,145 @@
1
+ // app/cms/components/ContentLanguageSwitcher.tsx
2
+ "use client";
3
+
4
+ import React, { useEffect, useState } from 'react';
5
+ import { createClient } from '@nextblock-cms/db';
6
+ import Link from 'next/link';
7
+ import { Button } from '@nextblock-cms/ui';
8
+ import { Languages, CheckCircle } from 'lucide-react';
9
+ import type { Database } from '@nextblock-cms/db';
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuContent,
13
+ DropdownMenuItem,
14
+ DropdownMenuLabel,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "@nextblock-cms/ui";
18
+ import { cn } from '@nextblock-cms/utils'; // For conditional styling
19
+
20
+ type Language = Database['public']['Tables']['languages']['Row'];
21
+ type Page = Database['public']['Tables']['pages']['Row'];
22
+ type Post = Database['public']['Tables']['posts']['Row'];
23
+ interface ContentLanguageSwitcherProps {
24
+ currentItem: (Page | Post) & { language_code?: string; translation_group_id: string; }; // Must have translation_group_id
25
+ itemType: 'page' | 'post';
26
+ allSiteLanguages: Language[];
27
+ }
28
+
29
+ interface TranslationVersion {
30
+ id: number; // Primary key of the specific language version
31
+ language_id: number;
32
+ language_code: string;
33
+ language_name: string;
34
+ title: string;
35
+ status: string;
36
+ slug: string; // The specific slug for this language version
37
+ }
38
+
39
+ export default function ContentLanguageSwitcher({
40
+ currentItem,
41
+ itemType,
42
+ allSiteLanguages,
43
+ }: ContentLanguageSwitcherProps) {
44
+ const [translations, setTranslations] = useState<TranslationVersion[]>([]);
45
+ const [isLoading, setIsLoading] = useState(true);
46
+ const supabase = createClient();
47
+
48
+ useEffect(() => {
49
+ if (!currentItem.translation_group_id || allSiteLanguages.length === 0) {
50
+ setIsLoading(false);
51
+ setTranslations([]);
52
+ return;
53
+ }
54
+
55
+ async function fetchTranslations() {
56
+ setIsLoading(true);
57
+ const table = itemType === 'page' ? 'pages' : 'posts';
58
+ const { data, error } = await supabase
59
+ .from(table)
60
+ .select('id, title, status, language_id, slug') // Fetch slug too
61
+ .eq('translation_group_id', currentItem.translation_group_id);
62
+
63
+ if (error) {
64
+ console.error(`Error fetching translations for ${itemType} group ${currentItem.translation_group_id}:`, error);
65
+ setTranslations([]);
66
+ } else if (data) {
67
+ const mappedTranslations = data.map(item => {
68
+ const langInfo = allSiteLanguages.find(l => l.id === item.language_id);
69
+ return {
70
+ id: item.id,
71
+ language_id: item.language_id,
72
+ language_code: langInfo?.code || 'unk',
73
+ language_name: langInfo?.name || 'Unknown',
74
+ title: item.title,
75
+ status: item.status,
76
+ slug: item.slug,
77
+ };
78
+ });
79
+ setTranslations(mappedTranslations);
80
+ }
81
+ setIsLoading(false);
82
+ }
83
+
84
+ fetchTranslations();
85
+ }, [currentItem.translation_group_id, itemType, supabase, allSiteLanguages]);
86
+
87
+ const currentLanguageName = allSiteLanguages.find(l => l.id === currentItem.language_id)?.name || currentItem.language_code;
88
+
89
+ if (allSiteLanguages.length <= 1 && !isLoading) {
90
+ return null; // Don't show switcher if only one language configured
91
+ }
92
+
93
+ return (
94
+ <DropdownMenu>
95
+ <DropdownMenuTrigger asChild>
96
+ <Button variant="outline" className="ml-auto" disabled={isLoading}>
97
+ <Languages className="mr-2 h-4 w-4" />
98
+ {isLoading ? "Loading..." : `Editing: ${currentLanguageName} (${currentItem.language_code?.toUpperCase()})`}
99
+ </Button>
100
+ </DropdownMenuTrigger>
101
+ <DropdownMenuContent align="end" className="w-72"> {/* Increased width */}
102
+ <DropdownMenuLabel>Switch to Edit Other Language Version</DropdownMenuLabel>
103
+ <DropdownMenuSeparator />
104
+ {allSiteLanguages.map(lang => {
105
+ const version = translations.find(t => t.language_id === lang.id);
106
+ const isCurrent = lang.id === currentItem.language_id;
107
+ // Link to create new translation if it doesn't exist
108
+ // This requires a more complex "create translation" flow or pre-created placeholders
109
+ const editUrl = version
110
+ ? `/cms/${itemType === 'page' ? 'pages' : 'posts'}/${version.id}/edit`
111
+ : `/cms/${itemType === 'page' ? 'pages' : 'posts'}/new?from_group=${currentItem.translation_group_id}&target_lang_id=${lang.id}&base_slug=${currentItem.slug}`; // Example URL for creating new translation
112
+
113
+ if (version) {
114
+ return (
115
+ <DropdownMenuItem key={lang.id} asChild disabled={isCurrent} className={cn(isCurrent && "bg-accent font-semibold")}>
116
+ <Link href={editUrl} className="w-full">
117
+ <div className="flex justify-between items-center w-full">
118
+ <span>{lang.name} ({lang.code.toUpperCase()})</span>
119
+ {isCurrent && <CheckCircle className="h-4 w-4 text-primary" />}
120
+ </div>
121
+ <div className="text-xs text-muted-foreground truncate" title={version.title}>
122
+ Slug: /{version.slug} - <span className="capitalize">{version.status}</span>
123
+ </div>
124
+ </Link>
125
+ </DropdownMenuItem>
126
+ );
127
+ } else {
128
+ // Offer to create a new translation (simplified link, full flow is more complex)
129
+ return (
130
+ <DropdownMenuItem key={lang.id} asChild className="opacity-75 hover:opacity-100">
131
+ <Link href={editUrl} className="w-full"> {/* Adjust URL for creating new */}
132
+ <div className="flex justify-between items-center w-full">
133
+ <span>{lang.name} ({lang.code.toUpperCase()})</span>
134
+ <span className="text-xs text-blue-500">(Create)</span>
135
+ </div>
136
+ <div className="text-xs text-muted-foreground">Not yet created</div>
137
+ </Link>
138
+ </DropdownMenuItem>
139
+ );
140
+ }
141
+ })}
142
+ </DropdownMenuContent>
143
+ </DropdownMenu>
144
+ );
145
+ }
@@ -0,0 +1,203 @@
1
+ "use client";
2
+
3
+ import React, { useState, useTransition } from "react";
4
+ import { Button } from "@nextblock-cms/ui";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ DialogClose,
14
+ } from "@nextblock-cms/ui";
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from "@nextblock-cms/ui";
22
+ import { Label } from "@nextblock-cms/ui";
23
+ import { copyBlocksFromLanguage } from "@/app/cms/blocks/actions";
24
+ import type { Database } from "@nextblock-cms/db";
25
+ import { useRouter } from "next/navigation";
26
+
27
+ type Language = Database['public']['Tables']['languages']['Row'];
28
+ import { AlertCircle, CheckCircle2, Copy as CopyIcon } from "lucide-react";
29
+
30
+ interface CopyContentFromLanguageProps {
31
+ parentId: number;
32
+ parentType: "page" | "post";
33
+ currentLanguageId: number;
34
+ translationGroupId: string;
35
+ allSiteLanguages: Language[];
36
+ }
37
+
38
+ export default function CopyContentFromLanguage({
39
+ parentId,
40
+ parentType,
41
+ currentLanguageId,
42
+ translationGroupId,
43
+ allSiteLanguages,
44
+ }: CopyContentFromLanguageProps) {
45
+ const [isModalOpen, setIsModalOpen] = useState(false);
46
+ const [selectedSourceLanguageId, setSelectedSourceLanguageId] = useState<
47
+ number | null
48
+ >(null);
49
+ const [message, setMessage] = useState<{
50
+ type: "success" | "error";
51
+ text: string;
52
+ } | null>(null);
53
+ const [isPending, startTransition] = useTransition();
54
+ const router = useRouter();
55
+
56
+ const handleCopy = async () => {
57
+ if (!selectedSourceLanguageId) {
58
+ setMessage({
59
+ type: "error",
60
+ text: "Please select a source language.",
61
+ });
62
+ return;
63
+ }
64
+
65
+ setMessage(null);
66
+
67
+ startTransition(async () => {
68
+ try {
69
+ const result = await copyBlocksFromLanguage(
70
+ parentId,
71
+ parentType,
72
+ selectedSourceLanguageId,
73
+ currentLanguageId,
74
+ translationGroupId
75
+ );
76
+
77
+ if (result.success) {
78
+ setMessage({
79
+ type: "success",
80
+ text: "Content copied successfully. The page will now refresh.",
81
+ });
82
+ router.refresh();
83
+ // setIsModalOpen(false); // Refresh might close it, or parent re-render
84
+ } else {
85
+ setMessage({
86
+ type: "error",
87
+ text: result.error || "Failed to copy content. Please try again.",
88
+ });
89
+ }
90
+ } catch (error) {
91
+ console.error("Error copying content:", error);
92
+ setMessage({
93
+ type: "error",
94
+ text: "An unexpected error occurred. Please try again.",
95
+ });
96
+ }
97
+ });
98
+ };
99
+
100
+ const availableSourceLanguages = allSiteLanguages.filter(
101
+ (lang) => lang.id !== currentLanguageId
102
+ );
103
+
104
+ // Reset message when modal opens or source language changes
105
+ React.useEffect(() => {
106
+ if (isModalOpen) {
107
+ setMessage(null);
108
+ }
109
+ }, [isModalOpen, selectedSourceLanguageId]);
110
+
111
+ // Close modal on successful copy after refresh (if not already closed by refresh)
112
+ React.useEffect(() => {
113
+ if (message?.type === "success" && !isPending) {
114
+ const timer = setTimeout(() => {
115
+ setIsModalOpen(false);
116
+ }, 1500); // Give some time for the user to see the success message
117
+ return () => clearTimeout(timer);
118
+ }
119
+ }, [message, isPending]);
120
+
121
+
122
+ if (availableSourceLanguages.length === 0) {
123
+ return (
124
+ <Button variant="outline" size="sm" disabled title="No other languages available to copy from">
125
+ <CopyIcon className="h-4 w-4 mr-2" />
126
+ Copy Content...
127
+ </Button>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
133
+ <DialogTrigger asChild>
134
+ <Button variant="outline" size="sm" title="Copy content from another language">
135
+ <CopyIcon className="h-4 w-4 mr-2" />
136
+ Copy Content...
137
+ </Button>
138
+ </DialogTrigger>
139
+ <DialogContent>
140
+ <DialogHeader>
141
+ <DialogTitle>Copy Content from Another Language</DialogTitle>
142
+ <DialogDescription>
143
+ Select a source language. This will replace all existing blocks for the current language with blocks from the selected language. This action cannot be undone.
144
+ </DialogDescription>
145
+ </DialogHeader>
146
+
147
+ <div className="space-y-4 py-4">
148
+ <div className="space-y-2">
149
+ <Label htmlFor="sourceLanguageModal">Select Source Language</Label>
150
+ <Select
151
+ onValueChange={(value) => {
152
+ setSelectedSourceLanguageId(Number(value));
153
+ setMessage(null); // Clear message on new selection
154
+ }}
155
+ disabled={isPending}
156
+ value={selectedSourceLanguageId ? String(selectedSourceLanguageId) : undefined}
157
+ >
158
+ <SelectTrigger id="sourceLanguageModal" className="w-full">
159
+ <SelectValue placeholder="Select a language..." />
160
+ </SelectTrigger>
161
+ <SelectContent>
162
+ {availableSourceLanguages.map((lang) => (
163
+ <SelectItem key={lang.id} value={String(lang.id)}>
164
+ {lang.name}
165
+ </SelectItem>
166
+ ))}
167
+ </SelectContent>
168
+ </Select>
169
+ </div>
170
+
171
+ {message && (
172
+ <div
173
+ className={`mt-3 p-3 rounded-md text-sm flex items-center gap-2 ${
174
+ message.type === "success"
175
+ ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
176
+ : "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
177
+ }`}
178
+ >
179
+ {message.type === "success" ? (
180
+ <CheckCircle2 className="h-5 w-5" />
181
+ ) : (
182
+ <AlertCircle className="h-5 w-5" />
183
+ )}
184
+ {message.text}
185
+ </div>
186
+ )}
187
+ </div>
188
+
189
+ <DialogFooter>
190
+ <DialogClose asChild>
191
+ <Button variant="outline" onClick={() => setMessage(null)}>Cancel</Button>
192
+ </DialogClose>
193
+ <Button
194
+ onClick={handleCopy}
195
+ disabled={!selectedSourceLanguageId || isPending}
196
+ >
197
+ {isPending ? "Copying..." : "Copy Content & Replace"}
198
+ </Button>
199
+ </DialogFooter>
200
+ </DialogContent>
201
+ </Dialog>
202
+ );
203
+ }
@@ -0,0 +1,69 @@
1
+ // app/cms/components/LanguageFilterSelect.tsx
2
+ "use client";
3
+
4
+ import React from 'react';
5
+ import { useRouter, useSearchParams } from 'next/navigation';
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@nextblock-cms/ui";
13
+ import type { Database } from '@nextblock-cms/db';
14
+ import { Languages as LanguageIcon } from 'lucide-react';
15
+
16
+ type Language = Database['public']['Tables']['languages']['Row'];
17
+
18
+ interface LanguageFilterSelectProps {
19
+ allLanguages: Language[];
20
+ currentFilterLangId?: number; // The ID of the currently filtered language
21
+ basePath: string; // e.g., "/cms/pages" or "/cms/posts"
22
+ }
23
+
24
+ export default function LanguageFilterSelect({
25
+ allLanguages,
26
+ currentFilterLangId,
27
+ basePath,
28
+ }: LanguageFilterSelectProps) {
29
+ const router = useRouter();
30
+ const searchParams = useSearchParams();
31
+
32
+ const handleLanguageChange = (selectedLangId: string) => {
33
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
34
+
35
+ if (selectedLangId && selectedLangId !== "all") {
36
+ current.set("lang", selectedLangId);
37
+ } else {
38
+ current.delete("lang"); // Remove lang param to show all languages
39
+ }
40
+ const query = current.toString();
41
+ router.push(`${basePath}${query ? `?${query}` : ""}`);
42
+ };
43
+
44
+ if (allLanguages.length <= 1) {
45
+ return null; // Don't show filter if only one or no languages
46
+ }
47
+
48
+ return (
49
+ <div className="flex items-center gap-2">
50
+ <LanguageIcon className="h-4 w-4 text-muted-foreground" />
51
+ <Select
52
+ value={currentFilterLangId?.toString() || "all"}
53
+ onValueChange={handleLanguageChange}
54
+ >
55
+ <SelectTrigger className="w-[180px] h-9 text-xs sm:text-sm">
56
+ <SelectValue placeholder="Filter by language..." />
57
+ </SelectTrigger>
58
+ <SelectContent>
59
+ <SelectItem value="all">All Languages</SelectItem>
60
+ {allLanguages.map((lang) => (
61
+ <SelectItem key={lang.id} value={lang.id.toString()}>
62
+ {lang.name} ({lang.code.toUpperCase()})
63
+ </SelectItem>
64
+ ))}
65
+ </SelectContent>
66
+ </Select>
67
+ </div>
68
+ );
69
+ }