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,189 @@
|
|
|
1
|
+
// components/blocks/renderers/SectionBlockRenderer.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { SectionBlockContent } from "../../../lib/blocks/blockRegistry";
|
|
4
|
+
import { getBlockDefinition } from "../../../lib/blocks/blockRegistry";
|
|
5
|
+
import dynamic from "next/dynamic";
|
|
6
|
+
|
|
7
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
8
|
+
|
|
9
|
+
interface SectionBlockRendererProps {
|
|
10
|
+
content: SectionBlockContent;
|
|
11
|
+
languageId: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Container class mapping
|
|
15
|
+
const containerClasses = {
|
|
16
|
+
'full-width': 'w-full',
|
|
17
|
+
'container': 'container mx-auto px-4',
|
|
18
|
+
'container-sm': 'container mx-auto px-4 max-w-screen-sm',
|
|
19
|
+
'container-lg': 'container mx-auto px-4 max-w-screen-lg',
|
|
20
|
+
'container-xl': 'container mx-auto px-4 max-w-screen-xl'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Column grid classes
|
|
24
|
+
const columnClasses = {
|
|
25
|
+
1: 'grid-cols-1',
|
|
26
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
|
27
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
28
|
+
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Gap classes
|
|
32
|
+
const gapClasses = {
|
|
33
|
+
none: 'gap-0',
|
|
34
|
+
sm: 'gap-2',
|
|
35
|
+
md: 'gap-4',
|
|
36
|
+
lg: 'gap-6',
|
|
37
|
+
xl: 'gap-8'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Padding classes
|
|
41
|
+
const paddingClasses = {
|
|
42
|
+
none: '',
|
|
43
|
+
sm: 'py-2',
|
|
44
|
+
md: 'py-4',
|
|
45
|
+
lg: 'py-8',
|
|
46
|
+
xl: 'py-12'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Background style generator
|
|
50
|
+
function generateBackgroundStyles(background: SectionBlockContent['background']) {
|
|
51
|
+
const styles: React.CSSProperties = {};
|
|
52
|
+
let className = '';
|
|
53
|
+
|
|
54
|
+
switch (background.type) {
|
|
55
|
+
case 'theme': {
|
|
56
|
+
// Theme-based backgrounds using CSS classes
|
|
57
|
+
const themeClasses = {
|
|
58
|
+
primary: 'bg-primary text-primary-foreground',
|
|
59
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
60
|
+
muted: 'bg-muted text-muted-foreground',
|
|
61
|
+
accent: 'bg-accent text-accent-foreground',
|
|
62
|
+
destructive: 'bg-destructive text-destructive-foreground'
|
|
63
|
+
};
|
|
64
|
+
className = background.theme ? themeClasses[background.theme] || '' : '';
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'solid':
|
|
69
|
+
styles.backgroundColor = background.solid_color;
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'gradient':
|
|
73
|
+
if (background.gradient) {
|
|
74
|
+
const { type, direction, stops } = background.gradient;
|
|
75
|
+
const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
|
|
76
|
+
styles.background = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'image':
|
|
81
|
+
if (background.image) {
|
|
82
|
+
const imageUrl = `${R2_BASE_URL}/${background.image.object_key}`;
|
|
83
|
+
styles.backgroundSize = background.image.size || 'cover';
|
|
84
|
+
styles.backgroundPosition = background.image.position || 'center';
|
|
85
|
+
|
|
86
|
+
let finalBackgroundImage = `url(${imageUrl})`;
|
|
87
|
+
|
|
88
|
+
if (background.image.overlay && background.image.overlay.gradient) {
|
|
89
|
+
const { type, direction, stops } = background.image.overlay.gradient;
|
|
90
|
+
const gradientStops = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');
|
|
91
|
+
const gradient = `${type}-gradient(${direction || 'to right'}, ${gradientStops})`;
|
|
92
|
+
finalBackgroundImage = `${gradient}, ${finalBackgroundImage}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
styles.backgroundImage = finalBackgroundImage;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
// No background
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { styles, className };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Dynamic block renderer component
|
|
108
|
+
const DynamicNestedBlockRenderer: React.FC<{
|
|
109
|
+
block: SectionBlockContent['column_blocks'][0][0];
|
|
110
|
+
languageId: number;
|
|
111
|
+
}> = ({ block, languageId }) => {
|
|
112
|
+
const blockDefinition = getBlockDefinition(block.block_type);
|
|
113
|
+
|
|
114
|
+
if (!blockDefinition) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="p-2 border rounded bg-destructive/10 text-destructive text-sm">
|
|
117
|
+
<strong>Unsupported block type:</strong> {block.block_type}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create dynamic component with proper SSR handling
|
|
123
|
+
const RendererComponent = dynamic(
|
|
124
|
+
() => import(`./${blockDefinition.rendererComponentFilename}`),
|
|
125
|
+
{
|
|
126
|
+
loading: () => (
|
|
127
|
+
<div className="animate-pulse bg-muted rounded h-8"></div>
|
|
128
|
+
),
|
|
129
|
+
ssr: true,
|
|
130
|
+
}
|
|
131
|
+
) as React.ComponentType<any>;
|
|
132
|
+
|
|
133
|
+
// Handle different prop requirements for different renderers
|
|
134
|
+
if (block.block_type === 'posts_grid') {
|
|
135
|
+
return (
|
|
136
|
+
<RendererComponent
|
|
137
|
+
content={block.content}
|
|
138
|
+
languageId={languageId}
|
|
139
|
+
block={{ ...block, id: 0, language_id: languageId, order: 0, created_at: '', updated_at: '' }}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<RendererComponent
|
|
146
|
+
content={block.content}
|
|
147
|
+
languageId={languageId}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const SectionBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
153
|
+
content,
|
|
154
|
+
languageId,
|
|
155
|
+
}) => {
|
|
156
|
+
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
157
|
+
|
|
158
|
+
// Build CSS classes
|
|
159
|
+
const containerClass = containerClasses[content.container_type] || containerClasses.container;
|
|
160
|
+
const gridClass = columnClasses[content.responsive_columns.desktop] || columnClasses[3];
|
|
161
|
+
const gapClass = gapClasses[content.column_gap] || gapClasses.md;
|
|
162
|
+
const paddingTopClass = paddingClasses[content.padding.top] || paddingClasses.md;
|
|
163
|
+
const paddingBottomClass = paddingClasses[content.padding.bottom] || paddingClasses.md;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<section
|
|
167
|
+
className={`w-full ${paddingTopClass} ${paddingBottomClass} ${backgroundClassName}`.trim()}
|
|
168
|
+
style={styles}
|
|
169
|
+
>
|
|
170
|
+
<div className={containerClass}>
|
|
171
|
+
<div className={`grid ${gridClass} ${gapClass}`}>
|
|
172
|
+
{content.column_blocks.map((columnBlocks, columnIndex) => (
|
|
173
|
+
<div key={`column-${columnIndex}`} className="min-h-0 space-y-4">
|
|
174
|
+
{(Array.isArray(columnBlocks) ? columnBlocks : []).map((block, blockIndex) => (
|
|
175
|
+
<DynamicNestedBlockRenderer
|
|
176
|
+
key={`${block.block_type}-${columnIndex}-${blockIndex}`}
|
|
177
|
+
block={block}
|
|
178
|
+
languageId={languageId}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</section>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default SectionBlockRenderer;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import ClientTextBlockRenderer from "./ClientTextBlockRenderer";
|
|
4
|
+
|
|
5
|
+
export type TextBlockContent = {
|
|
6
|
+
html_content?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
interface TextBlockRendererProps {
|
|
10
|
+
content: TextBlockContent;
|
|
11
|
+
languageId: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function addNonceToInlineScripts(html: string, nonce: string): string {
|
|
15
|
+
if (!html || !nonce) return html || '';
|
|
16
|
+
// Add nonce to <script> tags that do not already have a nonce
|
|
17
|
+
// and do not have a src attribute (inline scripts)
|
|
18
|
+
return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_m, attrs) => {
|
|
19
|
+
return `<script nonce="${nonce}"${attrs}>`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TextBlockRenderer: React.FC<TextBlockRendererProps> = async ({ content, languageId }) => {
|
|
24
|
+
const hdrs = await headers();
|
|
25
|
+
const nonce = hdrs.get('x-nonce') || '';
|
|
26
|
+
const htmlWithNonce = content.html_content ? addNonceToInlineScripts(content.html_content, nonce) : '';
|
|
27
|
+
const patchedContent = { ...content, html_content: htmlWithNonce };
|
|
28
|
+
return <ClientTextBlockRenderer content={patchedContent} languageId={languageId} />;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default TextBlockRenderer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { validateBlockContent, VideoEmbedBlockContent } from "@/lib/blocks/blockRegistry";
|
|
3
|
+
|
|
4
|
+
interface VideoEmbedBlockRendererProps {
|
|
5
|
+
content: VideoEmbedBlockContent;
|
|
6
|
+
languageId: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const VideoEmbedBlockRenderer: React.FC<VideoEmbedBlockRendererProps> = ({
|
|
10
|
+
content,
|
|
11
|
+
languageId,
|
|
12
|
+
}) => {
|
|
13
|
+
void languageId;
|
|
14
|
+
// Optional: Validate content against registry schema
|
|
15
|
+
const validation = validateBlockContent("video_embed", content);
|
|
16
|
+
if (!validation.isValid) {
|
|
17
|
+
console.warn("Invalid video embed content:", validation.errors);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!content.url) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Convert YouTube URLs to embed format
|
|
25
|
+
const getEmbedUrl = (url: string) => {
|
|
26
|
+
const youtubeRegex = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/;
|
|
27
|
+
const match = url.match(youtubeRegex);
|
|
28
|
+
|
|
29
|
+
if (match) {
|
|
30
|
+
const videoId = match[1];
|
|
31
|
+
const params = new URLSearchParams();
|
|
32
|
+
if (content.autoplay) params.set('autoplay', '1');
|
|
33
|
+
if (!content.controls) params.set('controls', '0');
|
|
34
|
+
|
|
35
|
+
return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return url; // Return original URL if not YouTube
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="my-4">
|
|
43
|
+
{content.title && (
|
|
44
|
+
<h3 className="text-lg font-semibold mb-2">{content.title}</h3>
|
|
45
|
+
)}
|
|
46
|
+
<div className="relative aspect-video">
|
|
47
|
+
<iframe
|
|
48
|
+
src={getEmbedUrl(content.url)}
|
|
49
|
+
title={content.title || "Video"}
|
|
50
|
+
className="w-full h-full rounded-lg"
|
|
51
|
+
allowFullScreen
|
|
52
|
+
loading="lazy"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default VideoEmbedBlockRenderer;
|
package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface AlertWidgetRendererProps {
|
|
4
|
+
type: 'info' | 'warning' | 'notification' | 'danger';
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
align: 'left' | 'center' | 'right';
|
|
8
|
+
size: 'fit-content' | 'full-width';
|
|
9
|
+
textAlign: 'left' | 'center' | 'right';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AlertWidgetRenderer = ({ type, title, message, align, size, textAlign }: AlertWidgetRendererProps) => {
|
|
13
|
+
const alertClasses: { [key: string]: string } = {
|
|
14
|
+
info: 'bg-accent/60 text-accent-foreground border-2 border-accent',
|
|
15
|
+
warning: 'bg-warning/60 text-warning-foreground border-2 border-warning',
|
|
16
|
+
notification: 'bg-muted/60 text-muted-foreground border-2 border-muted-foreground',
|
|
17
|
+
danger: 'bg-destructive/60 text-destructive-foreground border-2 border-destructive',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sizeClasses: { [key: string]: string } = {
|
|
21
|
+
'fit-content': 'w-auto',
|
|
22
|
+
'full-width': 'w-full',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const alignClasses: { [key: string]: string } = {
|
|
26
|
+
left: 'text-left',
|
|
27
|
+
center: 'text-center',
|
|
28
|
+
right: 'text-right',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const textAlignClasses: { [key: string]: string } = {
|
|
32
|
+
left: 'text-left',
|
|
33
|
+
center: 'text-center',
|
|
34
|
+
right: 'text-right',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={`${alignClasses[align] || 'text-left'}`}>
|
|
39
|
+
<div
|
|
40
|
+
className={`inline-block rounded-lg border p-2 m-1 ${alertClasses[type] || alertClasses.info} ${
|
|
41
|
+
sizeClasses[size] || sizeClasses['fit-content']
|
|
42
|
+
} ${textAlignClasses[textAlign] || textAlignClasses.left}`}
|
|
43
|
+
>
|
|
44
|
+
<strong className="font-bold block">{title}</strong>
|
|
45
|
+
<span>{message}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default AlertWidgetRenderer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
interface CtaWidgetRendererProps {
|
|
6
|
+
text: string;
|
|
7
|
+
url: string;
|
|
8
|
+
style: 'primary' | 'secondary';
|
|
9
|
+
size: 'fit-content' | 'full-width';
|
|
10
|
+
textAlign: 'left' | 'center' | 'right';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CtaWidgetRenderer = ({ text, url, style, size, textAlign }: CtaWidgetRendererProps) => {
|
|
14
|
+
const buttonClasses: { [key: string]: string } = {
|
|
15
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
16
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const sizeClasses: { [key: string]: string } = {
|
|
20
|
+
'fit-content': 'w-auto',
|
|
21
|
+
'full-width': 'w-full',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const textAlignClasses: { [key: string]: string } = {
|
|
26
|
+
left: 'text-left',
|
|
27
|
+
center: 'text-center',
|
|
28
|
+
right: 'text-right',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={`p-2 ${textAlignClasses[textAlign] || textAlignClasses.left}`}>
|
|
33
|
+
<Link href={url} className={`inline-block px-4 py-2 rounded-md ${buttonClasses[style]} ${sizeClasses[size] || sizeClasses['fit-content']}`}>
|
|
34
|
+
{text}
|
|
35
|
+
</Link>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default CtaWidgetRenderer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Database } from '@nextblock-cms/db';
|
|
2
|
+
|
|
3
|
+
export type PostWithMediaDimensions = Database['public']['Tables']['posts']['Row'] & {
|
|
4
|
+
feature_image_url: string | null;
|
|
5
|
+
feature_image_width: number | null;
|
|
6
|
+
feature_image_height: number | null;
|
|
7
|
+
blur_data_url: string | null;
|
|
8
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
|
|
5
|
+
export function EnvVarWarning() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex gap-4 items-center">
|
|
8
|
+
<Badge variant={"outline"} className="font-normal">
|
|
9
|
+
Supabase environment variables required
|
|
10
|
+
</Badge>
|
|
11
|
+
<div className="flex gap-2">
|
|
12
|
+
<Button
|
|
13
|
+
asChild
|
|
14
|
+
size="sm"
|
|
15
|
+
variant={"outline"}
|
|
16
|
+
disabled
|
|
17
|
+
className="opacity-75 cursor-none pointer-events-none"
|
|
18
|
+
>
|
|
19
|
+
<Link href="/sign-in">Sign in</Link>
|
|
20
|
+
</Button>
|
|
21
|
+
<Button
|
|
22
|
+
asChild
|
|
23
|
+
size="sm"
|
|
24
|
+
variant={"default"}
|
|
25
|
+
disabled
|
|
26
|
+
className="opacity-75 cursor-none pointer-events-none"
|
|
27
|
+
>
|
|
28
|
+
<Link href="/sign-up">Sign up</Link>
|
|
29
|
+
</Button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Message =
|
|
2
|
+
| { success: string }
|
|
3
|
+
| { error: string }
|
|
4
|
+
| { message: string };
|
|
5
|
+
|
|
6
|
+
export function FormMessage({ message }: { message?: Message }) {
|
|
7
|
+
if (!message) return null;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-col gap-2 w-full max-w-md text-sm">
|
|
11
|
+
{"success" in message && message.success && (
|
|
12
|
+
<div className="text-foreground border-l-2 border-foreground px-4">
|
|
13
|
+
{message.success}
|
|
14
|
+
</div>
|
|
15
|
+
)}
|
|
16
|
+
{"error" in message && message.error && (
|
|
17
|
+
<div className="text-destructive-foreground border-l-2 border-destructive-foreground px-4">
|
|
18
|
+
{message.error}
|
|
19
|
+
</div>
|
|
20
|
+
)}
|
|
21
|
+
{"message" in message && message.message && (
|
|
22
|
+
<div className="text-foreground border-l-2 px-4">{message.message}</div>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { signOutAction } from "@/app/actions";
|
|
4
|
+
import { hasPublicEnvVars } from "@nextblock-cms/utils";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
7
|
+
import { Button } from "@nextblock-cms/ui";
|
|
8
|
+
import { useAuth } from "@/context/AuthContext";
|
|
9
|
+
import { useTranslations } from "@nextblock-cms/utils";
|
|
10
|
+
|
|
11
|
+
export default function AuthButton() {
|
|
12
|
+
const { user, profile } = useAuth();
|
|
13
|
+
const { t } = useTranslations();
|
|
14
|
+
const username = profile?.username || null;
|
|
15
|
+
|
|
16
|
+
if (!hasPublicEnvVars) {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<div className="flex gap-4 items-center">
|
|
20
|
+
<div>
|
|
21
|
+
<Badge
|
|
22
|
+
variant={"default"}
|
|
23
|
+
className="font-normal pointer-events-none"
|
|
24
|
+
>
|
|
25
|
+
{t('update_env_file_warning')}
|
|
26
|
+
</Badge>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex gap-2">
|
|
29
|
+
<Button
|
|
30
|
+
asChild
|
|
31
|
+
size="sm"
|
|
32
|
+
variant={"outline"}
|
|
33
|
+
disabled
|
|
34
|
+
className="opacity-75 cursor-none pointer-events-none"
|
|
35
|
+
>
|
|
36
|
+
<Link href="/sign-in">{t('sign_in')}</Link>
|
|
37
|
+
</Button>
|
|
38
|
+
<Button
|
|
39
|
+
asChild
|
|
40
|
+
size="sm"
|
|
41
|
+
variant={"default"}
|
|
42
|
+
disabled
|
|
43
|
+
className="opacity-75 cursor-none pointer-events-none"
|
|
44
|
+
>
|
|
45
|
+
<Link href="/sign-up">{t('sign_up')}</Link>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return user ? (
|
|
53
|
+
<div className="flex items-center gap-4">
|
|
54
|
+
{t('greeting', { username: username || user.email || 'User' })}
|
|
55
|
+
<form action={signOutAction}>
|
|
56
|
+
<Button type="submit" variant={"outline"}>
|
|
57
|
+
{t('sign_out')}
|
|
58
|
+
</Button>
|
|
59
|
+
</form>
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="flex gap-2">
|
|
63
|
+
<Button asChild size="sm" variant={"outline"}>
|
|
64
|
+
<Link href="/sign-in">{t('sign_in')}</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
<Button asChild size="sm" variant={"default"}>
|
|
67
|
+
<Link href="/sign-up">{t('sign_up')}</Link>
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
import { type ComponentProps } from "react";
|
|
5
|
+
import { useFormStatus } from "react-dom";
|
|
6
|
+
|
|
7
|
+
type Props = ComponentProps<typeof Button> & {
|
|
8
|
+
pendingText?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SubmitButton({
|
|
12
|
+
children,
|
|
13
|
+
pendingText = "Submitting...",
|
|
14
|
+
...props
|
|
15
|
+
}: Props) {
|
|
16
|
+
const { pending } = useFormStatus();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Button type="submit" aria-disabled={pending} {...props}>
|
|
20
|
+
{pending ? pendingText : children}
|
|
21
|
+
</Button>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@nextblock-cms/ui";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@nextblock-cms/ui";
|
|
11
|
+
import { Laptop, Moon, Sun } from "lucide-react";
|
|
12
|
+
import { useTheme } from "next-themes";
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
|
|
15
|
+
const ThemeSwitcher = () => {
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
const { theme, setTheme } = useTheme();
|
|
18
|
+
|
|
19
|
+
// useEffect only runs on the client, so now we can safely show the UI
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setMounted(true);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (!mounted) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ICON_SIZE = 16;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger asChild>
|
|
33
|
+
<Button variant="ghost" size={"sm"} aria-label="Theme Switcher">
|
|
34
|
+
{theme === "light" ? (
|
|
35
|
+
<Sun
|
|
36
|
+
key="light"
|
|
37
|
+
size={ICON_SIZE}
|
|
38
|
+
className={"text-muted-foreground"}
|
|
39
|
+
/>
|
|
40
|
+
) : theme === "dark" ? (
|
|
41
|
+
<Moon
|
|
42
|
+
key="dark"
|
|
43
|
+
size={ICON_SIZE}
|
|
44
|
+
className={"text-muted-foreground"}
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<Laptop
|
|
48
|
+
key="system"
|
|
49
|
+
size={ICON_SIZE}
|
|
50
|
+
className={"text-muted-foreground"}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
</Button>
|
|
54
|
+
</DropdownMenuTrigger>
|
|
55
|
+
<DropdownMenuContent className="w-content" align="start">
|
|
56
|
+
<DropdownMenuRadioGroup
|
|
57
|
+
value={theme}
|
|
58
|
+
onValueChange={(e) => setTheme(e)}
|
|
59
|
+
>
|
|
60
|
+
<DropdownMenuRadioItem className="flex gap-2" value="light">
|
|
61
|
+
<Sun size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
62
|
+
<span>Light</span>
|
|
63
|
+
</DropdownMenuRadioItem>
|
|
64
|
+
<DropdownMenuRadioItem className="flex gap-2" value="dark">
|
|
65
|
+
<Moon size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
66
|
+
<span>Dark</span>
|
|
67
|
+
</DropdownMenuRadioItem>
|
|
68
|
+
<DropdownMenuRadioItem className="flex gap-2" value="system">
|
|
69
|
+
<Laptop size={ICON_SIZE} className="text-muted-foreground" />{" "}
|
|
70
|
+
<span>System</span>
|
|
71
|
+
</DropdownMenuRadioItem>
|
|
72
|
+
</DropdownMenuRadioGroup>
|
|
73
|
+
</DropdownMenuContent>
|
|
74
|
+
</DropdownMenu>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export { ThemeSwitcher };
|