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
package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DropdownMenuItem } from "@nextblock-cms/ui";
|
|
4
|
+
import { Trash2, ShieldAlert } from "lucide-react";
|
|
5
|
+
import { deleteLanguage } from "../actions"; // Server action
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
|
+
import { useTransition, useState } from 'react';
|
|
8
|
+
import { ConfirmationModal } from "@/app/cms/components/ConfirmationModal";
|
|
9
|
+
|
|
10
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
11
|
+
|
|
12
|
+
interface DeleteLanguageClientButtonProps {
|
|
13
|
+
language: Language;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function DeleteLanguageClientButton({ language }: DeleteLanguageClientButtonProps) {
|
|
17
|
+
const [isPending, startTransition] = useTransition();
|
|
18
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
19
|
+
|
|
20
|
+
// A more robust check for "is_default and is the only default language" should ideally
|
|
21
|
+
// be handled by the server action or by passing allLanguages to this component.
|
|
22
|
+
// For UI hint, this is a simplified check. The server action has the final say.
|
|
23
|
+
const isDefaultLanguage = language.is_default;
|
|
24
|
+
|
|
25
|
+
const handleDeleteConfirm = () => {
|
|
26
|
+
// The server-side `deleteLanguage` action already checks if it's the default
|
|
27
|
+
// and if it's the only language. This client-side check is for immediate UX.
|
|
28
|
+
if (isDefaultLanguage) {
|
|
29
|
+
// The server action has a more robust check for "only default"
|
|
30
|
+
// For now, a simple alert for any default language.
|
|
31
|
+
alert("Cannot delete the default language. Please set another language as default first, or ensure this is not the only language.");
|
|
32
|
+
setIsModalOpen(false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
startTransition(async () => {
|
|
37
|
+
const result = await deleteLanguage(language.id); // Call the server action
|
|
38
|
+
if (result?.error) {
|
|
39
|
+
alert(`Error: ${result.error}`);
|
|
40
|
+
// In a real app, use a toast or a more integrated notification system
|
|
41
|
+
}
|
|
42
|
+
// Revalidation and redirection are handled by the server action itself.
|
|
43
|
+
setIsModalOpen(false);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<DropdownMenuItem
|
|
50
|
+
className={`hover:!bg-red-50 dark:hover:!bg-red-700/20 ${
|
|
51
|
+
isDefaultLanguage // Visually hint if it's default, server action has final say on deletability
|
|
52
|
+
? "text-muted-foreground cursor-not-allowed hover:!text-muted-foreground"
|
|
53
|
+
: "text-red-600 hover:!text-red-600 cursor-pointer"
|
|
54
|
+
}`}
|
|
55
|
+
onSelect={(e) => e.preventDefault()} // Prevent menu closing immediately
|
|
56
|
+
onClick={() => !isPending && !isDefaultLanguage && setIsModalOpen(true)}
|
|
57
|
+
disabled={isPending || isDefaultLanguage} // Disable if pending or if it's the default language
|
|
58
|
+
>
|
|
59
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
60
|
+
{isPending ? "Deleting..." : "Delete"}
|
|
61
|
+
{isDefaultLanguage && (
|
|
62
|
+
<span title="This is the default language. Deletion might be restricted.">
|
|
63
|
+
<ShieldAlert className="ml-auto h-4 w-4 text-amber-500" />
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</DropdownMenuItem>
|
|
67
|
+
<ConfirmationModal
|
|
68
|
+
isOpen={isModalOpen}
|
|
69
|
+
onClose={() => setIsModalOpen(false)}
|
|
70
|
+
onConfirm={handleDeleteConfirm}
|
|
71
|
+
title="Are you sure?"
|
|
72
|
+
description="This will permanently delete the language. This action cannot be undone."
|
|
73
|
+
/>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// app/cms/settings/languages/components/LanguageForm.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useTransition, useEffect } from 'react';
|
|
5
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
6
|
+
import { Button } from '@nextblock-cms/ui';
|
|
7
|
+
import { Input } from '@nextblock-cms/ui';
|
|
8
|
+
import { Label } from '@nextblock-cms/ui';
|
|
9
|
+
import { Checkbox } from '@nextblock-cms/ui'; // Assuming shadcn/ui Checkbox
|
|
10
|
+
import type { Database } from "@nextblock-cms/db";
|
|
11
|
+
|
|
12
|
+
type Language = Database["public"]["Tables"]["languages"]["Row"];
|
|
13
|
+
import { useAuth } from '@/context/AuthContext';
|
|
14
|
+
|
|
15
|
+
interface LanguageFormProps {
|
|
16
|
+
language?: Language | null;
|
|
17
|
+
formAction: (formData: FormData) => Promise<{ error?: string } | void>;
|
|
18
|
+
actionButtonText?: string;
|
|
19
|
+
isEditing?: boolean;
|
|
20
|
+
allLanguages?: Language[]; // Pass all languages to check for "only default" scenario
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function LanguageForm({
|
|
24
|
+
language,
|
|
25
|
+
formAction,
|
|
26
|
+
actionButtonText = "Save Language",
|
|
27
|
+
isEditing = false,
|
|
28
|
+
allLanguages = []
|
|
29
|
+
}: LanguageFormProps) {
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const searchParams = useSearchParams();
|
|
32
|
+
const [isPending, startTransition] = useTransition();
|
|
33
|
+
const { isAdmin, isLoading: authLoading } = useAuth();
|
|
34
|
+
|
|
35
|
+
const [code, setCode] = useState(language?.code || "");
|
|
36
|
+
const [name, setName] = useState(language?.name || "");
|
|
37
|
+
const [isDefault, setIsDefault] = useState(language?.is_default || false);
|
|
38
|
+
const [isActive, setIsActive] = useState(language?.is_active ?? true);
|
|
39
|
+
|
|
40
|
+
const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const successMessage = searchParams.get('success');
|
|
44
|
+
const errorMessage = searchParams.get('error');
|
|
45
|
+
if (successMessage) setFormMessage({ type: 'success', text: successMessage });
|
|
46
|
+
else if (errorMessage) setFormMessage({ type: 'error', text: errorMessage });
|
|
47
|
+
}, [searchParams]);
|
|
48
|
+
|
|
49
|
+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
setFormMessage(null);
|
|
52
|
+
const formData = new FormData(event.currentTarget);
|
|
53
|
+
// Checkbox value needs to be explicitly set if not checked
|
|
54
|
+
if (!isDefault) {
|
|
55
|
+
formData.delete('is_default'); // Remove if not checked, action handles "on" or missing
|
|
56
|
+
} else {
|
|
57
|
+
formData.set('is_default', 'on');
|
|
58
|
+
}
|
|
59
|
+
if (isActive) {
|
|
60
|
+
formData.set('is_active', 'on');
|
|
61
|
+
} else {
|
|
62
|
+
formData.delete('is_active');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
startTransition(async () => {
|
|
67
|
+
const result = await formAction(formData);
|
|
68
|
+
if (result?.error) {
|
|
69
|
+
setFormMessage({ type: 'error', text: result.error });
|
|
70
|
+
}
|
|
71
|
+
// Success is handled by redirect with query param in server action
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (authLoading) return <div>Loading...</div>;
|
|
76
|
+
if (!isAdmin) return <div>Access Denied. Admin role required.</div>;
|
|
77
|
+
|
|
78
|
+
const isTheOnlyDefaultLanguage = isEditing && language?.is_default && allLanguages.filter(l => l.is_default).length === 1;
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
83
|
+
{formMessage && (
|
|
84
|
+
<div
|
|
85
|
+
className={`p-3 rounded-md text-sm ${
|
|
86
|
+
formMessage.type === 'success'
|
|
87
|
+
? 'bg-green-100 text-green-700 border border-green-200'
|
|
88
|
+
: 'bg-red-100 text-red-700 border border-red-200'
|
|
89
|
+
}`}
|
|
90
|
+
>
|
|
91
|
+
{formMessage.text}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
<div>
|
|
95
|
+
<Label htmlFor="code">Language Code (e.g., en, fr-CA)</Label>
|
|
96
|
+
<Input
|
|
97
|
+
id="code"
|
|
98
|
+
name="code"
|
|
99
|
+
value={code}
|
|
100
|
+
onChange={(e) => setCode(e.target.value)}
|
|
101
|
+
required
|
|
102
|
+
maxLength={10}
|
|
103
|
+
className="mt-1"
|
|
104
|
+
placeholder="en"
|
|
105
|
+
/>
|
|
106
|
+
<p className="text-xs text-muted-foreground mt-1">Short, unique BCP 47 language tag.</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div>
|
|
110
|
+
<Label htmlFor="name">Display Name</Label>
|
|
111
|
+
<Input
|
|
112
|
+
id="name"
|
|
113
|
+
name="name"
|
|
114
|
+
value={name}
|
|
115
|
+
onChange={(e) => setName(e.target.value)}
|
|
116
|
+
required
|
|
117
|
+
className="mt-1"
|
|
118
|
+
placeholder="English"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="flex items-center space-x-2 pt-2">
|
|
123
|
+
<Checkbox
|
|
124
|
+
id="is_default"
|
|
125
|
+
name="is_default"
|
|
126
|
+
checked={isDefault}
|
|
127
|
+
onCheckedChange={(checked) => setIsDefault(checked as boolean)}
|
|
128
|
+
disabled={isTheOnlyDefaultLanguage && isDefault} // Prevent unchecking the only default
|
|
129
|
+
/>
|
|
130
|
+
<Label htmlFor="is_default" className="font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
131
|
+
Set as Default Language
|
|
132
|
+
</Label>
|
|
133
|
+
</div>
|
|
134
|
+
{isTheOnlyDefaultLanguage && isDefault && (
|
|
135
|
+
<p className="text-xs text-amber-600">This is the only default language. To change, set another language as default.</p>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<div className="flex items-center space-x-2 pt-2">
|
|
139
|
+
<Checkbox
|
|
140
|
+
id="is_active"
|
|
141
|
+
name="is_active"
|
|
142
|
+
checked={isActive}
|
|
143
|
+
onCheckedChange={(checked) => setIsActive(checked as boolean)}
|
|
144
|
+
/>
|
|
145
|
+
<Label htmlFor="is_active" className="font-normal leading-none">
|
|
146
|
+
Language is Active
|
|
147
|
+
</Label>
|
|
148
|
+
</div>
|
|
149
|
+
<p className="text-xs text-muted-foreground -mt-1">Inactive languages are hidden from public view but still available for content management.</p>
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
<div className="flex justify-end space-x-3 pt-4">
|
|
153
|
+
<Button
|
|
154
|
+
type="button"
|
|
155
|
+
variant="outline"
|
|
156
|
+
onClick={() => router.push("/cms/settings/languages")}
|
|
157
|
+
disabled={isPending}
|
|
158
|
+
>
|
|
159
|
+
Cancel
|
|
160
|
+
</Button>
|
|
161
|
+
<Button type="submit" disabled={isPending || authLoading}>
|
|
162
|
+
{isPending ? "Saving..." : actionButtonText}
|
|
163
|
+
</Button>
|
|
164
|
+
</div>
|
|
165
|
+
</form>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// app/cms/settings/languages/new/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import LanguageForm from "../components/LanguageForm";
|
|
4
|
+
import { createLanguage } from "../actions";
|
|
5
|
+
import { createClient } from "@nextblock-cms/db/server"; // To fetch all languages for the form logic
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
|
+
|
|
8
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
9
|
+
|
|
10
|
+
async function getAllLanguages(): Promise<Language[]> {
|
|
11
|
+
const supabase = createClient();
|
|
12
|
+
const { data, error } = await supabase.from("languages").select("*");
|
|
13
|
+
if (error) {
|
|
14
|
+
console.error("Error fetching languages for new page form:", error);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
return data || [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default async function NewLanguagePage() {
|
|
22
|
+
const allLanguages = await getAllLanguages();
|
|
23
|
+
return (
|
|
24
|
+
<div className="max-w-xl mx-auto">
|
|
25
|
+
<h1 className="text-2xl font-bold mb-6">Add New Language</h1>
|
|
26
|
+
<LanguageForm
|
|
27
|
+
formAction={createLanguage}
|
|
28
|
+
actionButtonText="Add Language"
|
|
29
|
+
isEditing={false}
|
|
30
|
+
allLanguages={allLanguages}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// app/cms/settings/languages/page.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@nextblock-cms/ui";
|
|
14
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { MoreHorizontal, PlusCircle, Edit3, Languages as LanguagesIcon, ShieldAlert } from "lucide-react";
|
|
16
|
+
import {
|
|
17
|
+
DropdownMenu,
|
|
18
|
+
DropdownMenuContent,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
} from "@nextblock-cms/ui";
|
|
23
|
+
import type { Database } from "@nextblock-cms/db";
|
|
24
|
+
import DeleteLanguageClientButton from './components/DeleteLanguageButton';
|
|
25
|
+
|
|
26
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
27
|
+
|
|
28
|
+
async function getLanguages(): Promise<Language[]> {
|
|
29
|
+
const supabase = createClient();
|
|
30
|
+
const { data, error } = await supabase
|
|
31
|
+
.from("languages")
|
|
32
|
+
.select("*")
|
|
33
|
+
.order("name", { ascending: true });
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
console.error("Error fetching languages:", error);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return data || [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function CmsLanguagesListPage() {
|
|
43
|
+
const languages = await getLanguages();
|
|
44
|
+
// The following line for searchParams will cause an error during static generation or if window is not defined.
|
|
45
|
+
// It's better to pass searchParams as props if needed from the page component.
|
|
46
|
+
// For this specific page, success messages are handled by redirect query params which Next.js makes available in page props.
|
|
47
|
+
// Let's assume `props.searchParams.success` would be used if passed.
|
|
48
|
+
// const searchParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
49
|
+
// const successMessage = searchParams.get('success');
|
|
50
|
+
const successMessage: string | null = null;
|
|
51
|
+
// If you need to read searchParams, ensure your page component accepts them:
|
|
52
|
+
// export default async function CmsLanguagesListPage({ searchParams }: { searchParams?: { success?: string } }) {
|
|
53
|
+
// successMessage = searchParams?.success ?? null;
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="w-full">
|
|
59
|
+
<div className="flex justify-between items-center mb-6">
|
|
60
|
+
<h1 className="text-2xl font-semibold">Manage Languages</h1>
|
|
61
|
+
<Button variant="default" asChild>
|
|
62
|
+
<Link href="/cms/settings/languages/new">
|
|
63
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Add New Language
|
|
64
|
+
</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{successMessage && (
|
|
69
|
+
<div className="mb-4 p-3 rounded-md text-sm bg-green-100 text-green-700 border border-green-200">
|
|
70
|
+
{decodeURIComponent(successMessage)}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{languages.length === 0 ? (
|
|
75
|
+
<div className="text-center py-10 border rounded-lg">
|
|
76
|
+
<LanguagesIcon className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
77
|
+
<h3 className="mt-2 text-sm font-medium text-foreground">No languages configured</h3>
|
|
78
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
79
|
+
Add languages to support multilingual content.
|
|
80
|
+
</p>
|
|
81
|
+
<div className="mt-6">
|
|
82
|
+
<Button asChild>
|
|
83
|
+
<Link href="/cms/settings/languages/new">
|
|
84
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Add Language
|
|
85
|
+
</Link>
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<div className="rounded-lg border overflow-hidden">
|
|
91
|
+
<Table>
|
|
92
|
+
<TableHeader>
|
|
93
|
+
<TableRow>
|
|
94
|
+
<TableHead className="w-[200px]">Name</TableHead>
|
|
95
|
+
<TableHead>Code</TableHead>
|
|
96
|
+
<TableHead>Default</TableHead>
|
|
97
|
+
<TableHead>Created At</TableHead>
|
|
98
|
+
<TableHead className="text-right w-[80px]">Actions</TableHead>
|
|
99
|
+
</TableRow>
|
|
100
|
+
</TableHeader>
|
|
101
|
+
<TableBody>
|
|
102
|
+
{languages.map((lang) => (
|
|
103
|
+
<TableRow key={lang.id}>
|
|
104
|
+
<TableCell className="font-medium">{lang.name}</TableCell>
|
|
105
|
+
<TableCell>
|
|
106
|
+
<Badge variant="secondary">{lang.code}</Badge>
|
|
107
|
+
</TableCell>
|
|
108
|
+
<TableCell>
|
|
109
|
+
{lang.is_default ? (
|
|
110
|
+
<Badge className="bg-blue-500 hover:bg-blue-600 text-white">Default</Badge>
|
|
111
|
+
) : (
|
|
112
|
+
<span className="text-xs text-muted-foreground">-</span>
|
|
113
|
+
)}
|
|
114
|
+
</TableCell>
|
|
115
|
+
<TableCell className="text-xs text-muted-foreground">
|
|
116
|
+
{lang.created_at ? new Date(lang.created_at).toLocaleDateString() : '-'}
|
|
117
|
+
</TableCell>
|
|
118
|
+
<TableCell className="text-right">
|
|
119
|
+
<DropdownMenu>
|
|
120
|
+
<DropdownMenuTrigger asChild>
|
|
121
|
+
<Button variant="ghost" size="icon">
|
|
122
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
123
|
+
<span className="sr-only">Language actions</span>
|
|
124
|
+
</Button>
|
|
125
|
+
</DropdownMenuTrigger>
|
|
126
|
+
<DropdownMenuContent align="end">
|
|
127
|
+
<DropdownMenuItem asChild>
|
|
128
|
+
<Link href={`/cms/settings/languages/${lang.id}/edit`} className="flex items-center cursor-pointer">
|
|
129
|
+
<Edit3 className="mr-2 h-4 w-4" /> Edit
|
|
130
|
+
</Link>
|
|
131
|
+
</DropdownMenuItem>
|
|
132
|
+
<DropdownMenuSeparator />
|
|
133
|
+
<DeleteLanguageClientButton language={lang} />
|
|
134
|
+
</DropdownMenuContent>
|
|
135
|
+
</DropdownMenu>
|
|
136
|
+
</TableCell>
|
|
137
|
+
</TableRow>
|
|
138
|
+
))}
|
|
139
|
+
</TableBody>
|
|
140
|
+
</Table>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
<div className="mt-6 p-4 border border-amber-300 bg-amber-50 dark:bg-amber-900/30 rounded-lg">
|
|
144
|
+
<div className="flex items-start">
|
|
145
|
+
<ShieldAlert className="h-5 w-5 text-amber-600 dark:text-amber-400 mr-3 flex-shrink-0 mt-0.5" />
|
|
146
|
+
<div>
|
|
147
|
+
<h4 className="text-sm font-semibold text-amber-700 dark:text-amber-300">Important Note on Deleting Languages</h4>
|
|
148
|
+
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
|
|
149
|
+
Deleting a language is a destructive action. All content (pages, posts, blocks, navigation items) specifically associated with that language will be permanently deleted due to database cascade rules. Please ensure this is intended before proceeding. You cannot delete the current default language if it is the only one.
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import LogoForm from '../../components/LogoForm'
|
|
2
|
+
import { updateLogo, getLogoById } from '../../actions'
|
|
3
|
+
import { notFound } from 'next/navigation'
|
|
4
|
+
|
|
5
|
+
export default async function EditLogoPage(props: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const params = await props.params
|
|
7
|
+
const logo = await getLogoById(params.id)
|
|
8
|
+
|
|
9
|
+
if (!logo) {
|
|
10
|
+
notFound()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<h1 className="text-2xl font-semibold mb-6">Edit Logo</h1>
|
|
16
|
+
<LogoForm logo={logo} action={updateLogo as any} />
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use server'
|
|
2
|
+
|
|
3
|
+
import { createClient } from '@nextblock-cms/db/server'
|
|
4
|
+
import { revalidatePath } from 'next/cache'
|
|
5
|
+
import { redirect } from 'next/navigation'
|
|
6
|
+
import type { Logo } from './types'
|
|
7
|
+
|
|
8
|
+
export async function createLogo(payload: {
|
|
9
|
+
name: string
|
|
10
|
+
media_id: string
|
|
11
|
+
}): Promise<{ success: boolean; error?: string }> {
|
|
12
|
+
const supabase = createClient()
|
|
13
|
+
|
|
14
|
+
const { error } = await supabase.from('logos').insert(payload)
|
|
15
|
+
|
|
16
|
+
if (error) {
|
|
17
|
+
console.error('Error creating logo:', error)
|
|
18
|
+
return { success: false, error: error.message }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
revalidatePath('/cms/settings/logos')
|
|
22
|
+
return { success: true }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function updateLogo(payload: {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
media_id: string
|
|
29
|
+
}) {
|
|
30
|
+
const supabase = createClient()
|
|
31
|
+
const { id, ...data } = payload
|
|
32
|
+
|
|
33
|
+
const { error } = await supabase.from('logos').update(data).eq('id', id)
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
console.error('Error updating logo:', error)
|
|
37
|
+
// Optionally, handle the error more gracefully
|
|
38
|
+
// redirect('/error?message=Could not update logo')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
revalidatePath('/cms/settings/logos')
|
|
43
|
+
revalidatePath(`/cms/settings/logos/${id}/edit`)
|
|
44
|
+
redirect('/cms/settings/logos')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function deleteLogo(id: string) {
|
|
48
|
+
const supabase = createClient()
|
|
49
|
+
|
|
50
|
+
const { error } = await supabase.from('logos').delete().eq('id', id)
|
|
51
|
+
|
|
52
|
+
if (error) {
|
|
53
|
+
console.error('Error deleting logo:', error)
|
|
54
|
+
// Optionally, handle the error more gracefully
|
|
55
|
+
// redirect('/error?message=Could not delete logo')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
revalidatePath('/cms/settings/logos')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getLogos() {
|
|
63
|
+
const supabase = createClient()
|
|
64
|
+
const { data, error } = await supabase
|
|
65
|
+
.from('logos')
|
|
66
|
+
.select('*, media:media_id(*)')
|
|
67
|
+
.order('created_at', { ascending: false })
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
console.error('Error fetching logos:', error.message)
|
|
71
|
+
throw new Error(`Failed to fetch logos: ${error.message}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return data
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getLogoById(id: string) {
|
|
78
|
+
const supabase = createClient()
|
|
79
|
+
const { data, error } = await supabase
|
|
80
|
+
.from('logos')
|
|
81
|
+
.select('*, media:media_id(*)')
|
|
82
|
+
.eq('id', id)
|
|
83
|
+
.single()
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
console.error(`Error fetching logo by id ${id}:`, error.message)
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return data
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function getActiveLogo(): Promise<Logo | null> {
|
|
94
|
+
const supabase = createClient()
|
|
95
|
+
|
|
96
|
+
const { data, error } = await supabase
|
|
97
|
+
.from('logos')
|
|
98
|
+
.select(
|
|
99
|
+
`
|
|
100
|
+
*,
|
|
101
|
+
media:media_id (*)
|
|
102
|
+
`
|
|
103
|
+
)
|
|
104
|
+
.order('created_at', { ascending: false })
|
|
105
|
+
.limit(1)
|
|
106
|
+
.maybeSingle()
|
|
107
|
+
|
|
108
|
+
if (error) {
|
|
109
|
+
console.error('Error fetching active logo:', error.message)
|
|
110
|
+
throw new Error(`Failed to fetch active logo: ${error.message}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return data as Logo | null
|
|
114
|
+
}
|