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.
- package/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- 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
|
+
}
|