From 6e5783e644dca55b71c1dc6c8e8f73f467741a19 Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Sat, 29 Aug 2026 15:45:18 -0400 Subject: [PATCH] Initial react editor Next.js template --- .gitignore | 8 + README.md | 12 + app.globals.json | 17 + app/editor/page.tsx | 6 + app/globals.css | 59 + app/layout.tsx | 19 + app/page.json | 20 + app/page.tsx | 9 + components.json | 21 + components/page-editor.tsx | 20 + components/page-render.tsx | 11 + components/ui/button.tsx | 30 + components/website/footer.config.tsx | 19 + components/website/footer.tsx | 13 + components/website/header.config.tsx | 34 + components/website/header.tsx | 46 + components/website/starter.config.tsx | 20 + components/website/starter.tsx | 15 + config/root.tsx | 21 + config/site.ts | 9 + config/types.ts | 19 + editor.config.tsx | 20 + lib/page-metadata.ts | 23 + lib/publish-page.ts | 33 + lib/utils.ts | 6 + next-env.d.ts | 6 + next.config.js | 15 + package.json | 34 + postcss.config.mjs | 5 + public/next.svg | 1 + tsconfig.json | 27 + yarn.lock | 3594 +++++++++++++++++++++++++ 32 files changed, 4192 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.globals.json create mode 100644 app/editor/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.json create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/page-editor.tsx create mode 100644 components/page-render.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/website/footer.config.tsx create mode 100644 components/website/footer.tsx create mode 100644 components/website/header.config.tsx create mode 100644 components/website/header.tsx create mode 100644 components/website/starter.config.tsx create mode 100644 components/website/starter.tsx create mode 100644 config/root.tsx create mode 100644 config/site.ts create mode 100644 config/types.ts create mode 100644 editor.config.tsx create mode 100644 lib/page-metadata.ts create mode 100644 lib/publish-page.ts create mode 100644 lib/utils.ts create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/next.svg create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f7305b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.next/ +node_modules/ +.env* +!.env.example +*.log +*.tsbuildinfo +.DS_Store +.yarn/install-state.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..21bf04d --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/app.globals.json b/app.globals.json new file mode 100644 index 0000000..bf9180d --- /dev/null +++ b/app.globals.json @@ -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" + } + } +} diff --git a/app/editor/page.tsx b/app/editor/page.tsx new file mode 100644 index 0000000..e5d9d07 --- /dev/null +++ b/app/editor/page.tsx @@ -0,0 +1,6 @@ +import PageEditor from '@/components/page-editor'; +import page from '../page.json'; + +export default function EditorPage() { + return ; +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..46745cc --- /dev/null +++ b/app/globals.css @@ -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); + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..a899e47 --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/app/page.json b/app/page.json new file mode 100644 index 0000000..76e05bc --- /dev/null +++ b/app/page.json @@ -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 } + ] +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..98b9832 --- /dev/null +++ b/app/page.tsx @@ -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 ; +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..26ad91e --- /dev/null +++ b/components.json @@ -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" + } +} diff --git a/components/page-editor.tsx b/components/page-editor.tsx new file mode 100644 index 0000000..433eee4 --- /dev/null +++ b/components/page-editor.tsx @@ -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 }) { + 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 ; +} diff --git a/components/page-render.tsx b/components/page-render.tsx new file mode 100644 index 0000000..f421662 --- /dev/null +++ b/components/page-render.tsx @@ -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 ; +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..b95d7b6 --- /dev/null +++ b/components/ui/button.tsx @@ -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 & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'button'; + return ; +} + +export { Button, buttonVariants }; diff --git a/components/website/footer.config.tsx b/components/website/footer.config.tsx new file mode 100644 index 0000000..b7b546a --- /dev/null +++ b/components/website/footer.config.tsx @@ -0,0 +1,19 @@ +import type { ComponentConfig } from '@reacteditor/core'; +import { PanelBottom } from 'lucide-react'; +import Footer, { type FooterProps } from './footer'; + +const footerConfig: ComponentConfig = { + label: 'Footer', + icon: , + category: 'navigation', + global: true, + defaultProps: { + copyright: '© 2026 Next.js Editor Starter', + }, + fields: { + copyright: { label: 'Copyright', type: 'text', contentEditable: true }, + }, + render: (props) =>