Template
Initial react editor Next.js template
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.next/
|
||||||
|
node_modules/
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
|
.DS_Store
|
||||||
|
.yarn/install-state.gz
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# React Editor Next.js
|
||||||
|
|
||||||
|
A minimal Next.js template with React Editor, Tailwind CSS v4, and shadcn/ui.
|
||||||
|
|
||||||
|
The homepage contains one editable Starter component, plus synced global Header and Footer components. Open `/editor` to edit and publish the page JSON.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"props": {
|
||||||
|
"siteName": "Next.js",
|
||||||
|
"links": [
|
||||||
|
{ "label": "Home", "url": "/" },
|
||||||
|
{ "label": "Editor", "url": "/editor" },
|
||||||
|
{ "label": "Next.js Docs", "url": "https://nextjs.org/docs" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"props": {
|
||||||
|
"copyright": "© 2026 Next.js Editor Starter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import PageEditor from '@/components/page-editor';
|
||||||
|
import page from '../page.json';
|
||||||
|
|
||||||
|
export default function EditorPage() {
|
||||||
|
return <PageEditor pagePath="app/page.json" page={page} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--font-sans: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
width: min(100% - 2rem, 72rem);
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.page-shell {
|
||||||
|
width: min(100% - 4rem, 72rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { site } from '@/config/site';
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
metadataBase: new URL(site.url),
|
||||||
|
title: site.name,
|
||||||
|
description: site.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"props": {
|
||||||
|
"title": "Next.js Editor Starter",
|
||||||
|
"description": "A minimal Next.js template powered by React Editor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{ "type": "header", "props": { "id": "header" }, "synced": true },
|
||||||
|
{
|
||||||
|
"type": "starter",
|
||||||
|
"props": {
|
||||||
|
"id": "starter",
|
||||||
|
"logoUrl": "/next.svg",
|
||||||
|
"message": "To edit this page navigate to /editor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "type": "footer", "props": { "id": "footer" }, "synced": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import PageRender from '@/components/page-render';
|
||||||
|
import { pageMetadata } from '@/lib/page-metadata';
|
||||||
|
import page from './page.json';
|
||||||
|
|
||||||
|
export const metadata = pageMetadata(page, { path: '/' });
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <PageRender page={page} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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} />;
|
||||||
|
}
|
||||||
@@ -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} />;
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { RootConfig } from '@reacteditor/core';
|
||||||
|
|
||||||
|
export type RootProps = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Root: RootConfig<{ props: RootProps }> = {
|
||||||
|
global: false,
|
||||||
|
defaultProps: {
|
||||||
|
title: 'Next.js Editor Starter',
|
||||||
|
description: 'A minimal Next.js template powered by React Editor.',
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
title: { label: 'Page title', type: 'text' },
|
||||||
|
description: { label: 'Meta description', type: 'textarea' },
|
||||||
|
},
|
||||||
|
render: ({ children }) => <>{children}</>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Root;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const site = {
|
||||||
|
name: 'Next.js Editor Starter',
|
||||||
|
description: 'A minimal Next.js template powered by React Editor.',
|
||||||
|
url:
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL ??
|
||||||
|
(process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||||
|
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||||
|
: 'http://localhost:3000'),
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Config, Data } from '@reacteditor/core';
|
||||||
|
import type { HeaderProps } from '@/components/website/header';
|
||||||
|
import type { FooterProps } from '@/components/website/footer';
|
||||||
|
import type { StarterProps } from '@/components/website/starter';
|
||||||
|
import type { RootProps } from './root';
|
||||||
|
|
||||||
|
export type Components = {
|
||||||
|
header: HeaderProps;
|
||||||
|
footer: FooterProps;
|
||||||
|
starter: StarterProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserConfig = Config<{
|
||||||
|
components: Components;
|
||||||
|
root: RootProps;
|
||||||
|
categories: ['navigation', 'content'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type UserData = Data<Components, RootProps>;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import Root from '@/config/root';
|
||||||
|
import type { UserConfig } from '@/config/types';
|
||||||
|
import headerConfig from '@/components/website/header.config';
|
||||||
|
import footerConfig from '@/components/website/footer.config';
|
||||||
|
import starterConfig from '@/components/website/starter.config';
|
||||||
|
|
||||||
|
export const appConfig: UserConfig = {
|
||||||
|
root: Root,
|
||||||
|
categories: {
|
||||||
|
navigation: { title: 'Navigation' },
|
||||||
|
content: { title: 'Content' },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
header: headerConfig,
|
||||||
|
footer: footerConfig,
|
||||||
|
starter: starterConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default appConfig;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { site } from '@/config/site';
|
||||||
|
|
||||||
|
interface EditorPage {
|
||||||
|
root?: {
|
||||||
|
props?: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageMetadata(page: EditorPage, overrides: { path?: string } = {}): Metadata {
|
||||||
|
const props = page.root?.props ?? {};
|
||||||
|
const title = props.title ?? site.name;
|
||||||
|
const description = props.description ?? site.description;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: { absolute: title },
|
||||||
|
description,
|
||||||
|
alternates: overrides.path ? { canonical: overrides.path } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const GLOBALS_FILE = path.join(process.cwd(), 'app.globals.json');
|
||||||
|
|
||||||
|
export async function publishPage(pagePath: string, page: unknown) {
|
||||||
|
if (!page || typeof page !== 'object') {
|
||||||
|
return { error: 'Expected a page object.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const appDir = path.join(process.cwd(), 'app');
|
||||||
|
const file = path.resolve(process.cwd(), pagePath);
|
||||||
|
const relativeToApp = path.relative(appDir, file);
|
||||||
|
|
||||||
|
if (!relativeToApp || relativeToApp.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToApp) || path.basename(file) !== 'page.json') {
|
||||||
|
return { error: 'Invalid page file.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { globals, ...pageData } = page as Record<string, unknown>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(file, `${JSON.stringify(pageData, null, 2)}\n`, 'utf8');
|
||||||
|
if (globals && typeof globals === 'object') {
|
||||||
|
await writeFile(GLOBALS_FILE, `${JSON.stringify(globals, null, 2)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
return { file: path.relative(process.cwd(), file) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to write ${file}:`, error);
|
||||||
|
return { error: 'Could not save the page. Run the editor on a writable filesystem.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
reactStrictMode: true,
|
||||||
|
outputFileTracingRoot: dirname(fileURLToPath(import.meta.url)),
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
config.cache = { type: 'memory' };
|
||||||
|
if (isServer) {
|
||||||
|
config.optimization = config.optimization || {};
|
||||||
|
config.optimization.splitChunks = false;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "react-editor-nextjs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --webpack",
|
||||||
|
"build": "next build --webpack",
|
||||||
|
"start": "next start",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.3.3",
|
||||||
|
"@reacteditor/core": "0.0.42",
|
||||||
|
"@reacteditor/plugin-tailwind-cdn": "0.0.4",
|
||||||
|
"class-variance-authority": "0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.562.0",
|
||||||
|
"next": "16.2.10",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.18.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user