create-nextblock 0.2.33 → 0.2.35
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/package.json +1 -1
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
- package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
- package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
- package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +0 -1
- package/templates/nextblock-template/app/providers.tsx +2 -2
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
- package/templates/nextblock-template/eslint.config.mjs +35 -37
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
- package/templates/nextblock-template/next-env.d.ts +6 -6
- package/templates/nextblock-template/package.json +1 -1
|
@@ -59,7 +59,7 @@ export async function getTranslations() {
|
|
|
59
59
|
|
|
60
60
|
export async function updateTranslation(prevState: unknown, formData: FormData) {
|
|
61
61
|
const supabase = createClient();
|
|
62
|
-
const data = Object.fromEntries(formData
|
|
62
|
+
const data = Object.fromEntries(formData as any);
|
|
63
63
|
const key = data.key as string;
|
|
64
64
|
|
|
65
65
|
if (!key) {
|
|
@@ -13,7 +13,6 @@ export default async function EditLogoPage(props: { params: Promise<{ id: string
|
|
|
13
13
|
return (
|
|
14
14
|
<div>
|
|
15
15
|
<h1 className="text-2xl font-semibold mb-6">Edit Logo</h1>
|
|
16
|
-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
17
16
|
<LogoForm logo={logo} action={updateLogo as any} />
|
|
18
17
|
</div>
|
|
19
18
|
)
|
|
@@ -4,7 +4,7 @@ import { ThemeProvider } from "next-themes";
|
|
|
4
4
|
import { AuthProvider } from "@/context/AuthContext";
|
|
5
5
|
import { LanguageProvider } from "@/context/LanguageContext";
|
|
6
6
|
import { CurrentContentProvider } from "@/context/CurrentContentContext";
|
|
7
|
-
import { TranslationsProvider } from '@nextblock-cms/utils';
|
|
7
|
+
import { TranslationsProvider } from '@nextblock-cms/utils';
|
|
8
8
|
|
|
9
9
|
export function Providers({ children, ...props }: { children: React.ReactNode;[key: string]: any; }) {
|
|
10
10
|
const {
|
|
@@ -40,4 +40,4 @@ export function Providers({ children, ...props }: { children: React.ReactNode;[k
|
|
|
40
40
|
</LanguageProvider>
|
|
41
41
|
</AuthProvider>
|
|
42
42
|
);
|
|
43
|
-
}
|
|
43
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import dynamic from "next/dynamic";
|
|
4
4
|
import type { Database } from "@nextblock-cms/db";
|
|
5
|
-
import { getBlockDefinition, type SectionBlockContent, type BlockType } from "../lib/blocks/blockRegistry";
|
|
5
|
+
import { getBlockDefinition, type SectionBlockContent, type BlockType } from "../lib/blocks/blockRegistry";
|
|
6
6
|
|
|
7
7
|
type Block = Database['public']['Tables']['blocks']['Row'];
|
|
8
8
|
import HeroBlockRenderer from "./blocks/renderers/HeroBlockRenderer"; // Static import for LCP
|
|
@@ -50,14 +50,14 @@ const DynamicBlockRenderer: React.FC<DynamicBlockRendererProps> = ({
|
|
|
50
50
|
const RendererComponent = dynamic(
|
|
51
51
|
() => import(`./blocks/renderers/${blockDefinition.rendererComponentFilename}`),
|
|
52
52
|
{
|
|
53
|
-
loading: () => (
|
|
54
|
-
<div className="my-4 p-4 border rounded-lg">
|
|
55
|
-
<div className="h-8 w-1/2 mb-4 bg-muted/40 animate-pulse rounded" />
|
|
56
|
-
<div className="h-4 w-full mb-2 bg-muted/40 animate-pulse rounded" />
|
|
57
|
-
<div className="h-4 w-full mb-2 bg-muted/40 animate-pulse rounded" />
|
|
58
|
-
<div className="h-4 w-3/4 bg-muted/40 animate-pulse rounded" />
|
|
59
|
-
</div>
|
|
60
|
-
),
|
|
53
|
+
loading: () => (
|
|
54
|
+
<div className="my-4 p-4 border rounded-lg">
|
|
55
|
+
<div className="h-8 w-1/2 mb-4 bg-muted/40 animate-pulse rounded" />
|
|
56
|
+
<div className="h-4 w-full mb-2 bg-muted/40 animate-pulse rounded" />
|
|
57
|
+
<div className="h-4 w-full mb-2 bg-muted/40 animate-pulse rounded" />
|
|
58
|
+
<div className="h-4 w-3/4 bg-muted/40 animate-pulse rounded" />
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
61
|
ssr: true,
|
|
62
62
|
}
|
|
63
63
|
) as React.ComponentType<any>;
|
|
@@ -10,8 +10,8 @@ type Logo = Database['public']['Tables']['logos']['Row'] & { media: (Database['p
|
|
|
10
10
|
type NavigationItem = Database['public']['Tables']['navigation_items']['Row'];
|
|
11
11
|
import Image from 'next/image'
|
|
12
12
|
|
|
13
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
14
|
-
const FALLBACK_LOGO_PATH = '/images/nextblock-logo-small.webp';
|
|
13
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
14
|
+
const FALLBACK_LOGO_PATH = '/images/nextblock-logo-small.webp';
|
|
15
15
|
|
|
16
16
|
// Define a type for hierarchical navigation items
|
|
17
17
|
interface HierarchicalNavigationItem extends NavigationItem {
|
|
@@ -259,25 +259,25 @@ export default function ResponsiveNav({
|
|
|
259
259
|
href={homeLinkHref}
|
|
260
260
|
className="flex items-center space-x-2 rtl:space-x-reverse"
|
|
261
261
|
>
|
|
262
|
-
{logo && logo.media ? (
|
|
263
|
-
<Image
|
|
264
|
-
src={`${R2_BASE_URL}/${logo.media.object_key}`}
|
|
265
|
-
alt={logo.media.alt_text || siteTitle || 'Nextblock'}
|
|
266
|
-
width={logo.media.width || 100}
|
|
267
|
-
height={logo.media.height || 32}
|
|
268
|
-
className="h-14 w-auto object-contain"
|
|
269
|
-
priority
|
|
270
|
-
/>
|
|
271
|
-
) : (
|
|
272
|
-
<Image
|
|
273
|
-
src={FALLBACK_LOGO_PATH}
|
|
274
|
-
alt={siteTitle || 'Nextblock'}
|
|
275
|
-
width={120}
|
|
276
|
-
height={40}
|
|
277
|
-
className="h-14 w-auto object-contain"
|
|
278
|
-
priority
|
|
279
|
-
/>
|
|
280
|
-
)}
|
|
262
|
+
{logo && logo.media ? (
|
|
263
|
+
<Image
|
|
264
|
+
src={`${R2_BASE_URL}/${logo.media.object_key}`}
|
|
265
|
+
alt={logo.media.alt_text || siteTitle || 'Nextblock'}
|
|
266
|
+
width={logo.media.width || 100}
|
|
267
|
+
height={logo.media.height || 32}
|
|
268
|
+
className="h-14 w-auto object-contain"
|
|
269
|
+
priority
|
|
270
|
+
/>
|
|
271
|
+
) : (
|
|
272
|
+
<Image
|
|
273
|
+
src={FALLBACK_LOGO_PATH}
|
|
274
|
+
alt={siteTitle || 'Nextblock'}
|
|
275
|
+
width={120}
|
|
276
|
+
height={40}
|
|
277
|
+
className="h-14 w-auto object-contain"
|
|
278
|
+
priority
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
281
|
</Link>
|
|
282
282
|
{/* Desktop: Additional Nav items */}
|
|
283
283
|
<div className="hidden md:flex items-baseline font-semibold ml-6 space-x-1"> {/* Adjusted space-x for items with internal padding */}
|
|
@@ -375,4 +375,4 @@ export default function ResponsiveNav({
|
|
|
375
375
|
</div>
|
|
376
376
|
</>
|
|
377
377
|
);
|
|
378
|
-
}
|
|
378
|
+
}
|
|
@@ -24,7 +24,7 @@ const PostsGridBlock: React.FC<PostsGridBlockProps> = async ({ block, languageId
|
|
|
24
24
|
|
|
25
25
|
const supabase = createClient();
|
|
26
26
|
|
|
27
|
-
const { data: postsData, error: queryError, count } = await supabase
|
|
27
|
+
const { data: postsData, error: queryError, count } = await supabase
|
|
28
28
|
.from('posts')
|
|
29
29
|
.select('id, title, slug, excerpt, published_at, language_id, status, created_at, updated_at, translation_group_id, feature_image_id, feature_media_object:media!feature_image_id(object_key, width, height)', { count: 'exact' })
|
|
30
30
|
.eq('status', 'published')
|
|
@@ -40,18 +40,18 @@ const PostsGridBlock: React.FC<PostsGridBlockProps> = async ({ block, languageId
|
|
|
40
40
|
console.error("Error fetching initial posts directly in PostsGridBlock:", queryError);
|
|
41
41
|
postsError = queryError.message;
|
|
42
42
|
} else {
|
|
43
|
-
const buildMediaUrl = (objectKey?: string | null) => {
|
|
44
|
-
if (!objectKey) return null;
|
|
45
|
-
if (objectKey.startsWith('/')) return objectKey;
|
|
46
|
-
const base = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
47
|
-
return base ? `${base}/${objectKey}` : objectKey;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
initialPosts = (postsData as any)?.map((p: any) => {
|
|
43
|
+
const buildMediaUrl = (objectKey?: string | null) => {
|
|
44
|
+
if (!objectKey) return null;
|
|
45
|
+
if (objectKey.startsWith('/')) return objectKey;
|
|
46
|
+
const base = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
47
|
+
return base ? `${base}/${objectKey}` : objectKey;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
initialPosts = (postsData as any)?.map((p: any) => {
|
|
51
51
|
// feature_media_object is an object here, not an array, due to the query structure media!feature_image_id(object_key, width, height)
|
|
52
52
|
// Cast to 'unknown' then to the expected single object type to satisfy TypeScript, reflecting runtime reality.
|
|
53
|
-
const mediaObject = p.feature_media_object as unknown as { object_key: string; width?: number | null; height?: number | null; blur_data_url?: string | null } | null;
|
|
54
|
-
const imageUrl = buildMediaUrl(mediaObject?.object_key);
|
|
53
|
+
const mediaObject = p.feature_media_object as unknown as { object_key: string; width?: number | null; height?: number | null; blur_data_url?: string | null } | null;
|
|
54
|
+
const imageUrl = buildMediaUrl(mediaObject?.object_key);
|
|
55
55
|
return {
|
|
56
56
|
...p,
|
|
57
57
|
// Convert feature_media_object to array format to match the type
|
|
@@ -95,4 +95,4 @@ const PostsGridBlock: React.FC<PostsGridBlockProps> = async ({ block, languageId
|
|
|
95
95
|
);
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
export default PostsGridBlock;
|
|
98
|
+
export default PostsGridBlock;
|
package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import parse, { HTMLReactParserOptions, Element } from 'html-react-parser';
|
|
5
|
-
import AlertWidgetRenderer from "./inline/AlertWidgetRenderer";
|
|
6
|
-
import CtaWidgetRenderer from "./inline/CtaWidgetRenderer";
|
|
7
|
-
import type { TextBlockContent } from "./TextBlockRenderer";
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import parse, { HTMLReactParserOptions, Element } from 'html-react-parser';
|
|
5
|
+
import AlertWidgetRenderer from "./inline/AlertWidgetRenderer";
|
|
6
|
+
import CtaWidgetRenderer from "./inline/CtaWidgetRenderer";
|
|
7
|
+
import type { TextBlockContent } from "./TextBlockRenderer";
|
|
8
8
|
|
|
9
9
|
interface ClientTextBlockRendererProps {
|
|
10
10
|
content: TextBlockContent;
|
|
11
11
|
languageId: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const ClientTextBlockRenderer: React.FC<ClientTextBlockRendererProps> = ({ content, languageId }) => {
|
|
15
|
-
void languageId;
|
|
16
|
-
const options: HTMLReactParserOptions = {
|
|
17
|
-
replace: (domNode) => {
|
|
18
|
-
if (domNode instanceof Element && domNode.attribs) {
|
|
19
|
-
if (domNode.attribs['data-alert-widget'] !== undefined) {
|
|
20
|
-
const {
|
|
21
|
-
'data-type': type,
|
|
14
|
+
const ClientTextBlockRenderer: React.FC<ClientTextBlockRendererProps> = ({ content, languageId }) => {
|
|
15
|
+
void languageId;
|
|
16
|
+
const options: HTMLReactParserOptions = {
|
|
17
|
+
replace: (domNode) => {
|
|
18
|
+
if (domNode instanceof Element && domNode.attribs) {
|
|
19
|
+
if (domNode.attribs['data-alert-widget'] !== undefined) {
|
|
20
|
+
const {
|
|
21
|
+
'data-type': type,
|
|
22
22
|
'data-title': title,
|
|
23
23
|
'data-message': message,
|
|
24
24
|
'data-align': align,
|
|
@@ -55,15 +55,15 @@ const ClientTextBlockRenderer: React.FC<ClientTextBlockRendererProps> = ({ conte
|
|
|
55
55
|
/>
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div className="my-4 prose dark:prose-invert container mx-auto">
|
|
64
|
-
{parse(content.html_content || "", options)}
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="my-4 prose dark:prose-invert container mx-auto">
|
|
64
|
+
{parse(content.html_content || "", options)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
68
|
|
|
69
|
-
export default ClientTextBlockRenderer;
|
|
69
|
+
export default ClientTextBlockRenderer;
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
// components/blocks/renderers/HeroBlockRenderer.tsx
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React from "react";
|
|
3
3
|
import type { SectionBlockContent, Gradient } from "../../../lib/blocks/blockRegistry";
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
|
|
6
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
7
|
-
const HERO_BACKGROUND_DEFAULT_QUALITY = 60;
|
|
8
|
-
|
|
9
|
-
function resolveImageQuality(value: unknown, fallback: number): number {
|
|
10
|
-
if (value === undefined || value === null || value === '') {
|
|
11
|
-
return fallback;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const numeric =
|
|
15
|
-
typeof value === 'number'
|
|
16
|
-
? value
|
|
17
|
-
: Number.parseInt(String(value), 10);
|
|
18
|
-
|
|
19
|
-
if (!Number.isFinite(numeric)) {
|
|
20
|
-
return fallback;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const rounded = Math.round(numeric);
|
|
24
|
-
if (rounded < 1 || rounded > 100) {
|
|
25
|
-
return fallback;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return rounded;
|
|
29
|
-
}
|
|
6
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
7
|
+
const HERO_BACKGROUND_DEFAULT_QUALITY = 60;
|
|
8
|
+
|
|
9
|
+
function resolveImageQuality(value: unknown, fallback: number): number {
|
|
10
|
+
if (value === undefined || value === null || value === '') {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const numeric =
|
|
15
|
+
typeof value === 'number'
|
|
16
|
+
? value
|
|
17
|
+
: Number.parseInt(String(value), 10);
|
|
18
|
+
|
|
19
|
+
if (!Number.isFinite(numeric)) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rounded = Math.round(numeric);
|
|
24
|
+
if (rounded < 1 || rounded > 100) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return rounded;
|
|
29
|
+
}
|
|
30
30
|
|
|
31
31
|
interface SectionBlockRendererProps {
|
|
32
32
|
content: SectionBlockContent;
|
|
@@ -192,14 +192,14 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
192
192
|
content,
|
|
193
193
|
languageId,
|
|
194
194
|
}) => {
|
|
195
|
-
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
196
|
-
|
|
197
|
-
const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
|
|
198
|
-
|
|
199
|
-
if (backgroundImage) {
|
|
200
|
-
delete styles.backgroundImage;
|
|
201
|
-
}
|
|
202
|
-
const heroImageQuality = resolveImageQuality(backgroundImage?.quality, HERO_BACKGROUND_DEFAULT_QUALITY);
|
|
195
|
+
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
196
|
+
|
|
197
|
+
const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
|
|
198
|
+
|
|
199
|
+
if (backgroundImage) {
|
|
200
|
+
delete styles.backgroundImage;
|
|
201
|
+
}
|
|
202
|
+
const heroImageQuality = resolveImageQuality(backgroundImage?.quality, HERO_BACKGROUND_DEFAULT_QUALITY);
|
|
203
203
|
|
|
204
204
|
// Build CSS classes
|
|
205
205
|
const containerClass = containerClasses[content.container_type] || containerClasses.container;
|
|
@@ -229,13 +229,13 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
229
229
|
objectFit: backgroundImage.size || 'cover',
|
|
230
230
|
objectPosition: backgroundImage.position || 'center'
|
|
231
231
|
}}
|
|
232
|
-
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
|
|
233
|
-
priority={true}
|
|
234
|
-
fetchPriority="high"
|
|
235
|
-
quality={heroImageQuality}
|
|
236
|
-
{...imageProps}
|
|
237
|
-
/>
|
|
238
|
-
)}
|
|
232
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
|
|
233
|
+
priority={true}
|
|
234
|
+
fetchPriority="high"
|
|
235
|
+
quality={heroImageQuality}
|
|
236
|
+
{...imageProps}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
239
|
{backgroundImage?.overlay?.gradient && (
|
|
240
240
|
<div
|
|
241
241
|
className="absolute inset-0"
|
|
@@ -261,4 +261,4 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
261
261
|
);
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
-
export default HeroBlockRenderer;
|
|
264
|
+
export default HeroBlockRenderer;
|
|
@@ -19,12 +19,12 @@ interface ImageBlockRendererProps {
|
|
|
19
19
|
priority?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
|
|
23
|
-
content,
|
|
24
|
-
languageId,
|
|
25
|
-
priority = false,
|
|
26
|
-
}) => {
|
|
27
|
-
void languageId;
|
|
22
|
+
const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
|
|
23
|
+
content,
|
|
24
|
+
languageId,
|
|
25
|
+
priority = false,
|
|
26
|
+
}) => {
|
|
27
|
+
void languageId;
|
|
28
28
|
if (!content.media_id || !content.object_key) {
|
|
29
29
|
return (
|
|
30
30
|
<div className="my-4 p-4 border rounded text-center text-muted-foreground italic">
|
|
@@ -76,4 +76,4 @@ const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
|
|
|
76
76
|
);
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
export default ImageBlockRenderer;
|
|
79
|
+
export default ImageBlockRenderer;
|
|
@@ -1,78 +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 };
|
|
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 };
|
|
@@ -1,37 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...
|
|
30
|
-
'@next/next/no-html-link-for-pages': '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export default config;
|
|
1
|
+
import nx from '@nx/eslint-plugin';
|
|
2
|
+
import baseConfig from '../../eslint.config.mjs';
|
|
3
|
+
import nextPlugin from '@next/eslint-plugin-next';
|
|
4
|
+
|
|
5
|
+
const nextRules = {
|
|
6
|
+
...nextPlugin.configs.recommended.rules,
|
|
7
|
+
...nextPlugin.configs['core-web-vitals'].rules,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const config = [
|
|
11
|
+
...baseConfig,
|
|
12
|
+
...nx.configs['flat/react-typescript'],
|
|
13
|
+
{
|
|
14
|
+
ignores: ['.next/**/*', '**/next-env.d.ts', 'apps/nextblock/next-env.d.ts'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
files: [
|
|
18
|
+
'**/*.ts',
|
|
19
|
+
'**/*.tsx',
|
|
20
|
+
'**/*.js',
|
|
21
|
+
'**/*.jsx',
|
|
22
|
+
'**/*.mjs',
|
|
23
|
+
'**/*.cjs',
|
|
24
|
+
],
|
|
25
|
+
plugins: {
|
|
26
|
+
'@next/next': nextPlugin,
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
...nextRules,
|
|
30
|
+
'@next/next/no-html-link-for-pages': ['error', 'apps/nextblock/app'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export default config;
|