Initial react editor Next.js template

This commit is contained in:
Rami Bitar
2026-08-29 15:45:18 -04:00
commit 6e5783e644
32 changed files with 4192 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { useCallback, useMemo } from 'react';
import { Editor, outlinePlugin, type Data } from '@reacteditor/core';
import createTailwindCdnPlugin from '@reacteditor/plugin-tailwind-cdn';
import { appConfig } from '@/editor.config';
import { publishPage } from '@/lib/publish-page';
import globals from '@/app.globals.json';
const plugins = [outlinePlugin(), createTailwindCdnPlugin()];
export default function PageEditor({ pagePath, page }: { pagePath: string; page: Record<string, unknown> }) {
const data = useMemo(() => ({ ...page, globals }) as unknown as Data, [page]);
const handlePublish = useCallback(async (published: Data) => {
const result = await publishPage(pagePath, published);
if (result.error) throw new Error(result.error);
}, [pagePath]);
return <Editor theme="light" config={appConfig as never} data={data} plugins={plugins} onPublish={handlePublish} />;
}
+11
View File
@@ -0,0 +1,11 @@
'use client';
import { Render } from '@reacteditor/core/render';
import { appConfig } from '@/editor.config';
import globals from '@/app.globals.json';
export type PageData = { root?: unknown; content?: unknown; globals?: unknown };
export default function PageRender({ page }: { page: PageData }) {
return <Render config={appConfig as never} data={{ root: page.root, content: page.content, globals } as never} />;
}
+30
View File
@@ -0,0 +1,30 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border bg-background hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3 text-xs',
icon: 'size-9',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
);
function Button({ className, variant, size, asChild = false, ...props }: React.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'button';
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { Button, buttonVariants };
+19
View File
@@ -0,0 +1,19 @@
import type { ComponentConfig } from '@reacteditor/core';
import { PanelBottom } from 'lucide-react';
import Footer, { type FooterProps } from './footer';
const footerConfig: ComponentConfig<FooterProps> = {
label: 'Footer',
icon: <PanelBottom size={16} />,
category: 'navigation',
global: true,
defaultProps: {
copyright: '© 2026 Next.js Editor Starter',
},
fields: {
copyright: { label: 'Copyright', type: 'text', contentEditable: true },
},
render: (props) => <Footer {...props} />,
};
export default footerConfig;
+13
View File
@@ -0,0 +1,13 @@
export type FooterProps = {
copyright?: string;
};
export default function Footer({ copyright = '© 2026 Next.js Editor Starter' }: FooterProps) {
return (
<footer className="h-16 bg-background">
<div className="page-shell flex h-full items-center text-xs text-muted-foreground">
{copyright}
</div>
</footer>
);
}
+34
View File
@@ -0,0 +1,34 @@
import type { ComponentConfig } from '@reacteditor/core';
import { PanelTop } from 'lucide-react';
import Header, { type HeaderProps } from './header';
const headerConfig: ComponentConfig<HeaderProps> = {
label: 'Header',
icon: <PanelTop size={16} />,
category: 'navigation',
global: true,
defaultProps: {
siteName: 'Next.js',
links: [
{ label: 'Home', url: '/' },
{ label: 'Editor', url: '/editor' },
{ label: 'Next.js Docs', url: 'https://nextjs.org/docs' },
],
},
fields: {
siteName: { label: 'Site name', type: 'text', contentEditable: true },
links: {
label: 'Navigation links',
type: 'array',
defaultItemProps: { label: 'Link', url: '/' },
getItemSummary: (item) => item?.label || 'Link',
arrayFields: {
label: { label: 'Label', type: 'text', contentEditable: true },
url: { label: 'URL', type: 'text' },
},
},
},
render: (props) => <Header {...props} />,
};
export default headerConfig;
+46
View File
@@ -0,0 +1,46 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Menu, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
export type NavigationLink = { label: string; url: string };
export type HeaderProps = {
siteName?: string;
links?: NavigationLink[];
};
export default function Header({ siteName = 'Next.js', links = [] }: HeaderProps) {
const [open, setOpen] = useState(false);
return (
<header className="relative z-50 h-16 bg-background">
<div className="page-shell flex h-full items-center justify-between">
<Link href="/" className="text-sm font-semibold tracking-tight">{siteName}</Link>
<nav aria-label="Primary navigation" className="hidden items-center gap-7 md:flex">
{links.map((link) => (
<Link key={`${link.label}-${link.url}`} href={link.url} className="text-sm text-muted-foreground transition-colors hover:text-foreground">
{link.label}
</Link>
))}
</nav>
<Button type="button" variant="ghost" size="icon" className="md:hidden" aria-label={open ? 'Close navigation' : 'Open navigation'} aria-expanded={open} onClick={() => setOpen((value) => !value)}>
{open ? <X aria-hidden="true" /> : <Menu aria-hidden="true" />}
</Button>
</div>
{open && (
<nav aria-label="Mobile navigation" className="absolute inset-x-0 top-16 bg-background px-4 py-4 md:hidden">
<div className="page-shell flex flex-col gap-1">
{links.map((link) => (
<Link key={`${link.label}-${link.url}`} href={link.url} onClick={() => setOpen(false)} className="rounded-md px-3 py-2.5 text-sm hover:bg-muted">
{link.label}
</Link>
))}
</div>
</nav>
)}
</header>
);
}
+20
View File
@@ -0,0 +1,20 @@
import type { ComponentConfig } from '@reacteditor/core';
import { SquareDashed } from 'lucide-react';
import Starter, { type StarterProps } from './starter';
const starterConfig: ComponentConfig<StarterProps> = {
label: 'Starter',
icon: <SquareDashed size={16} />,
category: 'content',
defaultProps: {
logoUrl: '/next.svg',
message: 'To edit this page navigate to /editor.',
},
fields: {
logoUrl: { label: 'Logo', type: 'image' },
message: { label: 'Message', type: 'text', contentEditable: true },
},
render: (props) => <Starter {...props} />,
};
export default starterConfig;
+15
View File
@@ -0,0 +1,15 @@
export type StarterProps = {
logoUrl?: string;
message?: string;
};
export default function Starter({ logoUrl = '/next.svg', message = 'To edit this page navigate to /editor.' }: StarterProps) {
return (
<main className="flex min-h-[calc(100svh-8rem)] items-center justify-center px-6 py-20">
<div className="flex flex-col items-center text-center">
{logoUrl && <img src={logoUrl} alt="Next.js logo" className="h-auto w-[197px] object-contain" />}
<p className="mt-8 text-base text-muted-foreground sm:text-lg">{message}</p>
</div>
</main>
);
}