Template
Initial react editor website template
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().trim().min(2, 'Please enter your name.').max(100),
|
||||
email: z.string().trim().email('Please enter a valid email address.').max(254),
|
||||
company: z.string().trim().max(120).optional().default(''),
|
||||
message: z.string().trim().min(10, 'Please share a little more detail.').max(5000),
|
||||
website: z.string().max(0).optional().default(''),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const payload: unknown = await request.json();
|
||||
const result = contactSchema.safeParse(payload);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ error: result.error.issues[0]?.message ?? 'Please check the form and try again.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Store the validated contact data here.
|
||||
// For example: await db.contactSubmission.create({ data: result.data });
|
||||
|
||||
return NextResponse.json({ ok: true }, { status: 202 });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request.' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import page from '../page.json';
|
||||
|
||||
export default function ContactEditorPage() {
|
||||
return <PageEditor pagePath="app/contact/page.json" page={page} />;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Contact — Your Company",
|
||||
"description": "Tell us what you are working on and how we can help.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{ "type": "header", "props": { "id": "header" }, "synced": true },
|
||||
{
|
||||
"type": "contact-form",
|
||||
"props": {
|
||||
"id": "contact-form",
|
||||
"heading": "Let’s make something meaningful.",
|
||||
"description": "Tell us what you are working on, where you are stuck, or what you want to make possible.",
|
||||
"email": "hello@example.com",
|
||||
"location": "New York, NY",
|
||||
"nameLabel": "Name",
|
||||
"emailLabel": "Email",
|
||||
"companyLabel": "Company",
|
||||
"messageLabel": "How can we help?",
|
||||
"submitLabel": "Send message",
|
||||
"successMessage": "Thanks — we will be in touch soon."
|
||||
}
|
||||
},
|
||||
{ "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: '/contact' });
|
||||
|
||||
export default function ContactPage() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.22 0.02 245);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.22 0.02 245);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.22 0.02 245);
|
||||
--primary: oklch(0.23 0.025 245);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.96 0.008 245);
|
||||
--secondary-foreground: oklch(0.23 0.025 245);
|
||||
--muted: oklch(0.97 0.007 245);
|
||||
--muted-foreground: oklch(0.5 0.015 245);
|
||||
--accent: oklch(0.94 0.025 250);
|
||||
--accent-foreground: oklch(0.34 0.12 255);
|
||||
--destructive: oklch(0.58 0.22 27);
|
||||
--border: oklch(0.88 0.01 245);
|
||||
--input: oklch(0.88 0.01 245);
|
||||
--ring: oklch(0.56 0.2 255);
|
||||
--brand: oklch(0.56 0.21 255);
|
||||
--brand-soft: oklch(0.94 0.03 250);
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.15 0.014 264);
|
||||
--foreground: oklch(0.95 0.006 85);
|
||||
--card: oklch(0.19 0.016 264);
|
||||
--card-foreground: oklch(0.95 0.006 85);
|
||||
--popover: oklch(0.19 0.016 264);
|
||||
--popover-foreground: oklch(0.95 0.006 85);
|
||||
--primary: oklch(0.92 0.012 85);
|
||||
--primary-foreground: oklch(0.18 0.012 264);
|
||||
--secondary: oklch(0.24 0.018 264);
|
||||
--secondary-foreground: oklch(0.93 0.008 85);
|
||||
--muted: oklch(0.22 0.016 264);
|
||||
--muted-foreground: oklch(0.69 0.012 85);
|
||||
--accent: oklch(0.27 0.035 155);
|
||||
--accent-foreground: oklch(0.9 0.04 155);
|
||||
--destructive: oklch(0.7 0.18 27);
|
||||
--border: oklch(1 0 0 / 12%);
|
||||
--input: oklch(1 0 0 / 16%);
|
||||
--ring: oklch(0.7 0.1 155);
|
||||
--brand: oklch(0.7 0.16 250);
|
||||
--brand-soft: oklch(0.27 0.04 250);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-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-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-brand: var(--brand);
|
||||
--color-brand-soft: var(--brand-soft);
|
||||
--radius-sm: calc(var(--radius) - 6px);
|
||||
--radius-md: calc(var(--radius) - 3px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 6px);
|
||||
--font-sans: var(--font-outfit), ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-feature-settings: 'kern' 1, 'tnum' 1;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.animate-marquee {
|
||||
animation: marquee var(--marquee-duration, 28s) linear infinite;
|
||||
}
|
||||
|
||||
.marquee-reverse {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.marquee-pause:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
.animate-marquee { animation: none; transform: none; }
|
||||
}
|
||||
|
||||
.section-shell {
|
||||
width: min(100% - 2rem, 80rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section-shell { width: min(100% - 5rem, 80rem); }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Outfit } from 'next/font/google';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import { site } from '@/config/site';
|
||||
import './globals.css';
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-outfit',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(site.url),
|
||||
title: { default: site.name, template: `%s — ${site.name}` },
|
||||
description: site.description,
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
siteName: site.name,
|
||||
title: site.name,
|
||||
description: site.description,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: site.name,
|
||||
description: site.description,
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className={outfit.variable}>
|
||||
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Your Company — A clear website headline",
|
||||
"description": "A short description of your business and the value it provides.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{ "type": "header", "props": { "id": "header" }, "synced": true },
|
||||
{
|
||||
"type": "hero",
|
||||
"props": {
|
||||
"id": "hero",
|
||||
"heading": "A clear headline about what your business does.",
|
||||
"description": "Use this space to explain your value in one or two simple sentences.",
|
||||
"primaryLabel": "Get started",
|
||||
"primaryUrl": "/contact",
|
||||
"secondaryLabel": "",
|
||||
"secondaryUrl": "#features",
|
||||
"imageUrl": "https://api.dicebear.com/10.x/glass/svg",
|
||||
"imageAlt": "Abstract glass avatar illustration",
|
||||
"imagePosition": "right"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "logo-marquee",
|
||||
"props": {
|
||||
"id": "logos",
|
||||
"heading": "Trusted by teams at",
|
||||
"logos": [
|
||||
{ "name": "Logo One", "imageUrl": "" },
|
||||
{ "name": "Logo Two", "imageUrl": "" },
|
||||
{ "name": "Logo Three", "imageUrl": "" },
|
||||
{ "name": "Logo Four", "imageUrl": "" },
|
||||
{ "name": "Logo Five", "imageUrl": "" }
|
||||
],
|
||||
"speed": "normal",
|
||||
"direction": "left",
|
||||
"pauseOnHover": "yes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "features",
|
||||
"props": {
|
||||
"id": "features",
|
||||
"heading": "Explain the most important benefits of your business.",
|
||||
"description": "",
|
||||
"imageUrl": "https://api.dicebear.com/10.x/glass/svg",
|
||||
"imageAlt": "Abstract glass avatar illustration",
|
||||
"imagePosition": "left",
|
||||
"items": [
|
||||
{ "title": "First key benefit", "description": "Add a concise explanation of the value this benefit creates for your customers.", "ctaLabel": "", "ctaUrl": "#" },
|
||||
{ "title": "Second key benefit", "description": "Use a clear, outcome-focused description that is easy to scan.", "ctaLabel": "", "ctaUrl": "#" },
|
||||
{ "title": "Third key benefit", "description": "Give visitors one more reason to understand and trust your offer.", "ctaLabel": "", "ctaUrl": "#" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "stats",
|
||||
"props": {
|
||||
"id": "stats",
|
||||
"heading": "Use a few clear numbers to build confidence.",
|
||||
"stats": [
|
||||
{ "value": "10k+", "label": "Customers", "description": "A short note about this result." },
|
||||
{ "value": "42%", "label": "Growth", "description": "A short note about this result." },
|
||||
{ "value": "24/7", "label": "Availability", "description": "A short note about this result." },
|
||||
{ "value": "98%", "label": "Satisfaction", "description": "A short note about this result." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "testimonials",
|
||||
"props": {
|
||||
"id": "testimonials",
|
||||
"heading": "Share what customers say about your business.",
|
||||
"testimonials": [
|
||||
{ "quote": "Add a short customer quote that explains the value of your work.", "name": "Customer name", "role": "Role, Company", "avatarUrl": "" },
|
||||
{ "quote": "Use a second testimonial to reinforce the outcome customers can expect.", "name": "Customer name", "role": "Role, Company", "avatarUrl": "" },
|
||||
{ "quote": "Keep each quote focused, credible, and easy for visitors to scan.", "name": "Customer name", "role": "Role, Company", "avatarUrl": "" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "pricing",
|
||||
"props": {
|
||||
"id": "pricing",
|
||||
"heading": "Simple plans for different needs.",
|
||||
"description": "Replace these sample plans with pricing that fits your business.",
|
||||
"billingNote": "Prices shown in USD.",
|
||||
"plans": [
|
||||
{ "name": "Starter", "price": "$0", "period": "/ month", "description": "A short description of who this plan is for.", "features": "First included feature\nSecond included feature\nThird included feature", "ctaLabel": "Get started", "ctaUrl": "/contact", "badge": "", "highlighted": "no" },
|
||||
{ "name": "Team", "price": "$24", "period": "/ month", "description": "A short description of who this plan is for.", "features": "Everything in Starter\nAnother useful feature\nPriority support", "ctaLabel": "Get started", "ctaUrl": "/contact", "badge": "Popular", "highlighted": "yes" },
|
||||
{ "name": "Business", "price": "Custom", "period": "", "description": "A short description of who this plan is for.", "features": "Everything in Team\nAdvanced controls\nDedicated support", "ctaLabel": "Contact us", "ctaUrl": "/contact", "badge": "", "highlighted": "no" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "posts-carousel",
|
||||
"props": {
|
||||
"id": "posts-carousel",
|
||||
"heading": "Explore useful articles, guides, and stories.",
|
||||
"description": "",
|
||||
"posts": [
|
||||
{ "title": "Add the title of your first article or resource", "category": "Guide", "date": "August 20, 2026", "imageUrl": "https://api.dicebear.com/10.x/glass/svg", "imageAlt": "Abstract glass avatar illustration", "url": "#", "typeLabel": "Read" },
|
||||
{ "title": "Share an update that helps customers learn more", "category": "News", "date": "August 14, 2026", "imageUrl": "https://api.dicebear.com/10.x/glass/svg", "imageAlt": "Abstract glass avatar illustration", "url": "#", "typeLabel": "Read" },
|
||||
{ "title": "Feature a customer story or helpful perspective", "category": "Story", "date": "August 8, 2026", "imageUrl": "https://api.dicebear.com/10.x/glass/svg", "imageAlt": "Abstract glass avatar illustration", "url": "#", "typeLabel": "Read" },
|
||||
{ "title": "Use additional cards to demonstrate the carousel", "category": "Article", "date": "August 1, 2026", "imageUrl": "https://api.dicebear.com/10.x/glass/svg", "imageAlt": "Abstract glass avatar illustration", "url": "#", "typeLabel": "Read" }
|
||||
],
|
||||
"viewAllLabel": "View all",
|
||||
"viewAllUrl": "#"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "faq",
|
||||
"props": {
|
||||
"id": "faq",
|
||||
"heading": "Get answers to frequently asked questions.",
|
||||
"description": "",
|
||||
"items": [
|
||||
{ "question": "What does your business offer?", "answer": "Use this space to provide a clear, concise answer to a common customer question." },
|
||||
{ "question": "Who is this for?", "answer": "Describe the people, teams, or organizations that will benefit most from your offer." },
|
||||
{ "question": "How do I get started?", "answer": "Explain the simplest next step and what customers can expect after they begin." },
|
||||
{ "question": "Where can I learn more?", "answer": "Point visitors toward your contact page, documentation, or another helpful resource." }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cta",
|
||||
"props": {
|
||||
"id": "cta",
|
||||
"heading": "Add a clear call to action.",
|
||||
"description": "Use this space to explain what visitors should do next and why it matters.",
|
||||
"primaryLabel": "Get started",
|
||||
"primaryUrl": "/contact",
|
||||
"secondaryLabel": "",
|
||||
"secondaryUrl": "/contact"
|
||||
}
|
||||
},
|
||||
{ "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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user