Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions apps/ottabase-template-app-nextjs-homepage/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import { buildCriticalCSS } from '@ottabase/brand-engine';
import type { Metadata } from 'next';
import { siteContent, homePage } from '../config/landing.config';
import { generateBrandConfig } from '../lib/brand-server';
import './globals.css';
import { Providers } from './providers';

export const metadata: Metadata = {
title: 'Ottabase Next.js Homepage Template',
description: 'A barebone Next.js homepage template with OpenNext and Cloudflare Workers deployment',
title: homePage.title || siteContent.name,
description: homePage.metaDescription || siteContent.tagline,
keywords: ['nextjs', 'cloudflare', 'workers', 'opennext', 'homepage', 'template', 'brand-engine'],
robots: 'index, follow',
authors: [{ name: 'Ottabase' }],
authors: [{ name: siteContent.name }],
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
// Generate brand config server-side (SSR)
// Note: Using 'light' for initial SSR. BrandProvider will handle dynamic theme switching on client.
const brandConfig = generateBrandConfig('light');
const theme = brandConfig.brandKitsMap.default.theme;

// Generate critical CSS for SSR (prevents FOUC)
const criticalCSS = buildCriticalCSS(theme);

return (
<html lang="en" suppressHydrationWarning>
<head>
{/* Inject critical CSS for theme variables */}
<style id="brand-critical" dangerouslySetInnerHTML={{ __html: criticalCSS }} />
{/* Load fonts - only if URLs are defined */}
{theme.typography.heading.url && <link rel="stylesheet" href={theme.typography.heading.url} />}
{theme.typography.body.url && <link rel="stylesheet" href={theme.typography.body.url} />}
{theme.typography.handwriting.url && <link rel="stylesheet" href={theme.typography.handwriting.url} />}
Expand Down
92 changes: 7 additions & 85 deletions apps/ottabase-template-app-nextjs-homepage/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,10 @@
import { DarkModeToggle } from '@ottabase/ui-components/dark-mode-toggle';
import { Button } from '@ottabase/ui-shadcn';
import Link from 'next/link';
import { getLandingTheme, initOttaLanding, renderPage } from '@ottabase/ottalanding';
import { homePage, landingThemeId, siteContent } from '@/config/landing.config';

export default function HomePage() {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="flex max-w-3xl flex-col items-center gap-8 px-4 text-center">
{/* Dark Mode Toggle in corner */}
<div className="absolute right-5 top-5">
<DarkModeToggle type="button" title="Toggle dark/light mode" />
</div>

{/* Main Heading */}
<h1
className="mb-4 mt-32 bg-gradient-to-r from-cyan-500 to-fuchsia-500 bg-clip-text text-6xl font-bold text-transparent"
style={{
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
Ottabase Next.js Homepage Template
</h1>

{/* Description */}
<p className="mb-8 text-xl leading-relaxed text-muted-foreground">
A barebone Next.js homepage template with OpenNext and Cloudflare Workers deployment
</p>

<p className="text-lg leading-relaxed text-muted-foreground">
Built with <strong>Next.js 16</strong>, <strong>TypeScript</strong>, <strong>Tailwind CSS</strong>,
and <strong>Brand Engine</strong>. Perfect for creating beautiful, dynamic homepages deployed to
Cloudflare Workers.
</p>

{/* Action Buttons */}
<div className="mt-8 flex flex-wrap gap-4 justify-center">
<Button asChild size="lg">
<Link href="/about">About</Link>
</Button>
// Register built-in themes (atlas + mono) and set active
initOttaLanding({ defaultThemeId: landingThemeId });

<Button asChild size="lg" variant="secondary">
<Link href="/theme-demo">🎨 Theme Demo</Link>
</Button>

<Button asChild variant="outline" size="lg">
<a href="https://github.com/thinkdj/ottabase" target="_blank" rel="noopener noreferrer">
View on GitHub
</a>
</Button>
</div>

{/* Features Grid */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-2xl">
<div className="p-6 rounded-lg border border-border bg-card">
<h3 className="text-lg font-semibold mb-2">🚀 Dynamic Workers</h3>
<p className="text-sm text-muted-foreground">
Deploy to Cloudflare Workers with OpenNext for edge-optimized performance
</p>
</div>
<div className="p-6 rounded-lg border border-border bg-card">
<h3 className="text-lg font-semibold mb-2">🎨 Brand Engine</h3>
<p className="text-sm text-muted-foreground">
Built-in theme system with 8+ presets and full customization support
</p>
</div>
<div className="p-6 rounded-lg border border-border bg-card">
<h3 className="text-lg font-semibold mb-2">⚡ Next.js 16</h3>
<p className="text-sm text-muted-foreground">
Latest Next.js with App Router and React Server Components
</p>
</div>
<div className="p-6 rounded-lg border border-border bg-card">
<h3 className="text-lg font-semibold mb-2">🎯 Type Safe</h3>
<p className="text-sm text-muted-foreground">
Full TypeScript support for better developer experience
</p>
</div>
</div>

{/* Footer */}
<p className="mt-16 text-sm text-muted-foreground">
© {new Date().getFullYear()} Ottabase. Open Source.
</p>
</div>
</div>
);
export default function HomePage() {
const theme = getLandingTheme(landingThemeId)!;
return renderPage(theme, siteContent, homePage);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function ThemeDemo() {
</Button>
</div>

<h1 className="font-heading text-4xl font-bold text-foreground">Theme Demo: {config.themeBase}</h1>
<h1 className="font-heading text-4xl font-bold text-foreground">Theme Demo: {config.theme.name}</h1>

{/* Typography Demo */}
<section className="space-y-4 rounded-lg border border-border bg-card p-6">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client';

import { useState } from 'react';
import Link from 'next/link';
import { getLandingTheme, initOttaLanding, renderPage } from '@ottabase/ottalanding';
import { homePage, siteContent } from '@/config/landing.config';

// Ensure themes are registered on the client
initOttaLanding({ defaultThemeId: 'atlas' });

interface ThemePreviewClientProps {
themeName: string;
lightVars: Record<string, string>;
darkVars: Record<string, string>;
}

function LandingPage({ themeId }: { themeId: string }) {
const theme = getLandingTheme(themeId);
if (!theme) return <div className="p-8 text-center text-muted-foreground">Theme "{themeId}" not found</div>;
return renderPage(theme, siteContent, homePage);
}

export function ThemePreviewClient({ themeName, lightVars, darkVars }: ThemePreviewClientProps) {
const [template, setTemplate] = useState<'atlas' | 'mono' | 'saas'>('atlas');
const [scheme, setScheme] = useState<'light' | 'dark'>('light');

const currentVars = scheme === 'light' ? lightVars : darkVars;

return (
<>
{/* Fixed control bar — uses the host page theme, not the preview theme */}
<div className="fixed inset-x-0 top-0 z-[200] flex h-12 items-center gap-3 border-b border-zinc-200 bg-white/95 px-4 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-950/95">
<Link
href="/theme"
className="shrink-0 text-sm text-zinc-500 hover:text-zinc-900 transition-colors dark:text-zinc-400 dark:hover:text-zinc-100"
>
← Themes
</Link>
<span className="text-zinc-300 dark:text-zinc-700">|</span>
<span className="font-medium text-sm capitalize text-zinc-800 dark:text-zinc-200">{themeName}</span>

<div className="ml-auto flex items-center gap-2">
{/* Template toggle */}
<div className="flex rounded-md border border-zinc-200 dark:border-zinc-700 text-xs overflow-hidden">
<button
onClick={() => setTemplate('atlas')}
className={`px-3 py-1.5 transition-colors ${
template === 'atlas'
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900'
: 'text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-800'
}`}
>
Atlas
</button>
<button
onClick={() => setTemplate('mono')}
className={`px-3 py-1.5 transition-colors ${
template === 'mono'
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900'
: 'text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-800'
}`}
>
Mono
</button>
<button
onClick={() => setTemplate('saas')}
className={`px-3 py-1.5 transition-colors ${
template === 'saas'
? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900'
: 'text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-800'
}`}
>
SaaS
</button>
</div>

{/* Light/Dark toggle */}
<button
onClick={() => setScheme((s) => (s === 'light' ? 'dark' : 'light'))}
className="rounded-md border border-zinc-200 px-3 py-1.5 text-xs text-zinc-600 hover:bg-zinc-50 transition-colors dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800"
>
{scheme === 'light' ? 'Light' : 'Dark'}
</button>
</div>
</div>

{/* Preview — scoped to the selected theme */}
<div className={scheme === 'dark' ? 'dark' : ''} style={currentVars as React.CSSProperties}>
<div className="bg-background pt-12">
<LandingPage themeId={template} />
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { buildCSSVarMap, getThemeByName, registerBuiltInThemes, resolveTheme } from '@ottabase/brand-engine';
import { notFound } from 'next/navigation';
import { ThemePreviewClient } from './ThemePreviewClient';

registerBuiltInThemes();

interface ThemePreviewPageProps {
params: Promise<{ themeName: string }>;
}

export async function generateMetadata({ params }: ThemePreviewPageProps) {
const { themeName } = await params;
return {
title: `${themeName.charAt(0).toUpperCase() + themeName.slice(1)} theme — Ottabase`,
description: `Preview the ${themeName} brand theme applied to Atlas and Mono marketing templates.`,
};
}

export default async function ThemePreviewPage({ params }: ThemePreviewPageProps) {
const { themeName } = await params;

const theme = getThemeByName(themeName);
if (!theme) notFound();

// Resolve CSS variables for both light and dark modes
const lightResolved = resolveTheme({ base: theme, mode: 'light' });
const darkResolved = resolveTheme({ base: theme, mode: 'dark' });

const lightVars = buildCSSVarMap(lightResolved);
const darkVars = buildCSSVarMap(darkResolved);

return <ThemePreviewClient themeName={themeName} lightVars={lightVars} darkVars={darkVars} />;
}
61 changes: 61 additions & 0 deletions apps/ottabase-template-app-nextjs-homepage/app/theme/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { THEME_PRESET_ITEMS, registerBuiltInThemes } from '@ottabase/brand-engine';
import Link from 'next/link';

registerBuiltInThemes();

export const metadata = {
title: 'Theme Gallery — Ottabase',
description: 'Browse all 8 built-in brand themes for your Ottabase app.',
};

export default function ThemeGalleryPage() {
const themes = THEME_PRESET_ITEMS;

return (
<div className="min-h-screen bg-background text-foreground">
{/* Header */}
<header className="border-b border-border bg-background px-6 py-8">
<div className="mx-auto max-w-6xl">
<Link
href="/"
className="mb-6 inline-block text-sm text-muted-foreground hover:text-foreground transition-colors"
>
← Back to home
</Link>
<h1 className="font-heading text-4xl font-bold text-foreground">Theme Gallery</h1>
<p className="mt-3 text-lg text-muted-foreground">
8 built-in brand themes. Click any theme to preview a full marketing page.
</p>
</div>
</header>

{/* Theme grid */}
<main className="mx-auto max-w-6xl px-6 py-12">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{themes.map((theme) => (
<Link
key={theme.id}
href={`/theme/${theme.id}`}
className="group flex flex-col overflow-hidden rounded-xl border border-border bg-card transition-all hover:shadow-lg hover:-translate-y-0.5"
>
{/* Color swatch strip */}
<div className="flex h-24">
{theme.colors.map((color, i) => (
<div key={i} className="flex-1" style={{ backgroundColor: `hsl(${color})` }} />
))}
</div>

{/* Theme info */}
<div className="flex flex-col gap-1 p-4">
<h2 className="font-heading text-base font-semibold text-foreground capitalize">
{theme.name}
</h2>
<p className="text-xs text-muted-foreground">Preview Atlas, Mono &amp; SaaS templates →</p>
</div>
</Link>
))}
</div>
</main>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
// Brand configuration for the Next.js homepage template
// This is a simple configuration-driven approach without admin UI
/**
* Brand Engine configuration — controls visual styling (colors, typography, spacing).
* This is separate from landing content — Brand Engine = CSS vars/design tokens,
* ottalanding = page structure and content.
*/

import type { BrandTheme } from '@ottabase/brand-engine';
import { brandPreset } from './landing.config';

/**
* Default brand configuration
* You can customize this to match your brand
*/
export const brandConfig: Partial<BrandTheme> = {
name: 'artisan',
// You can override colors, typography, spacing, etc. here
// See @ottabase/brand-engine documentation for all available options
name: brandPreset,
};

/**
* Theme preset to use
* Available presets: 'default', 'neo', 'crisp', 'funky', 'artisan', 'midnight', 'rose', 'verdant'
*/
export const themePreset = 'artisan';
export const themePreset = brandPreset;
Loading