Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import tiged from 'tiged';
import Handlebars from 'handlebars';
import { execSync } from 'child_process';
import { resolveDependencies } from './dependencies.js';
import { generateNavigation } from './navigation.js';

// Register custom Handlebars helpers
Handlebars.registerHelper('eq', function (a, b) {
Expand Down Expand Up @@ -101,7 +102,10 @@ export async function generateProject(config) {
const configFilePath = path.join(projectPath, 'opusify.config.json');
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2));

// 5. Resolve dynamic dependencies based on user choices
// 5. Generate dynamic navigation based on navCount
generateNavigation(projectPath, config);

// 6. Resolve dynamic dependencies based on user choices
resolveDependencies(projectPath, config);

// 6. AUTOMATION PHASE: Install Dependencies
Expand Down
157 changes: 157 additions & 0 deletions src/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import fs from 'fs';
import path from 'path';

// Page pools for each template — ordered by priority
const PAGE_POOLS = {
portfolio: [
{ label: 'Home', href: '/', slug: null },
{ label: 'About', href: '/about', slug: 'about' },
{ label: 'Projects', href: '/projects', slug: 'projects' },
{ label: 'Skills', href: '/skills', slug: 'skills' },
{ label: 'Contact', href: '/contact', slug: 'contact' },
{ label: 'Blog', href: '/blog', slug: 'blog' },
{ label: 'Testimonials', href: '/testimonials', slug: 'testimonials' },
{ label: 'Resume', href: '/resume', slug: 'resume' },
{ label: 'Services', href: '/services', slug: 'services' },
],
ecommerce: [
{ label: 'Home', href: '/', slug: null },
{ label: 'Products', href: '/products', slug: 'products' },
{ label: 'Cart', href: '/cart', slug: 'cart' },
{ label: 'Account', href: '/account', slug: 'account' },
{ label: 'Wishlist', href: '/wishlist', slug: 'wishlist' },
{ label: 'Orders', href: '/orders', slug: 'orders' },
{ label: 'Categories', href: '/categories', slug: 'categories' },
{ label: 'Search', href: '/search', slug: 'search' },
{ label: 'Support', href: '/support', slug: 'support' },
],
school: [
{ label: 'Dashboard', href: '/', slug: null },
{ label: 'Students', href: '/students', slug: 'students' },
{ label: 'Courses', href: '/courses', slug: 'courses' },
{ label: 'Grades', href: '/grades', slug: 'grades' },
{ label: 'Attendance', href: '/attendance', slug: 'attendance' },
{ label: 'Schedule', href: '/schedule', slug: 'schedule' },
{ label: 'Payments', href: '/payments', slug: 'payments' },
{ label: 'Reports', href: '/reports', slug: 'reports' },
{ label: 'Staff', href: '/staff', slug: 'staff' },
],
saas: [
{ label: 'Dashboard', href: '/', slug: null },
{ label: 'Analytics', href: '/analytics', slug: 'analytics' },
{ label: 'Users', href: '/users', slug: 'users' },
{ label: 'Billing', href: '/billing', slug: 'billing' },
{ label: 'Settings', href: '/settings', slug: 'settings' },
{ label: 'Reports', href: '/reports', slug: 'reports' },
{ label: 'Integrations', href: '/integrations', slug: 'integrations' },
{ label: 'Team', href: '/team', slug: 'team' },
{ label: 'Support', href: '/support', slug: 'support' },
],
blog: [
{ label: 'Home', href: '/', slug: null },
{ label: 'Articles', href: '/articles', slug: 'articles' },
{ label: 'Categories', href: '/categories', slug: 'categories' },
{ label: 'Authors', href: '/authors', slug: 'authors' },
{ label: 'Newsletter', href: '/newsletter', slug: 'newsletter' },
{ label: 'About', href: '/about', slug: 'about' },
{ label: 'Tags', href: '/tags', slug: 'tags' },
{ label: 'Archive', href: '/archive', slug: 'archive' },
{ label: 'Contact', href: '/contact', slug: 'contact' },
],
};

function generateNavTs(selectedPages) {
return `export interface NavItem {
label: string;
href: string;
}

export const navLinks: NavItem[] = [
${selectedPages.map((p) => ` { label: '${p.label}', href: '${p.href}' },`).join('\n')}
];
`;
}

function generatePageComponent(page, config) {
return `export default function ${page.label.replace(/\s+/g, '')}Page() {
return (
<div className="min-h-screen bg-background p-6 lg:p-10">
<div className="max-w-5xl mx-auto">
<h1 className="text-3xl font-bold text-foreground mb-4">${page.label}</h1>
<p className="text-text-secondary text-lg">
This is the ${page.label} page for ${config.projectName}.
</p>
<div className="mt-8 border border-border rounded-theme p-8 bg-card">
<p className="text-text-secondary">
Content for this page will be added here. This placeholder was generated
by Opusify CLI based on your navigation configuration.
</p>
</div>
</div>
</div>
);
}
`;
}

function generateVitePageComponent(page, config) {
return `export default function ${page.label.replace(/\s+/g, '')}() {
return (
<div className="min-h-screen bg-background p-6 lg:p-10">
<div className="max-w-5xl mx-auto">
<h1 className="text-3xl font-bold text-foreground mb-4">${page.label}</h1>
<p className="text-text-secondary text-lg">
This is the ${page.label} page for ${config.projectName}.
</p>
<div className="mt-8 border border-border rounded-theme p-8 bg-card">
<p className="text-text-secondary">
Content for this page will be added here. This placeholder was generated
by Opusify CLI based on your navigation configuration.
</p>
</div>
</div>
</div>
);
}
`;
}

export function generateNavigation(projectPath, config) {
const pool = PAGE_POOLS[config.template];
if (!pool) return;

const navCount = Math.min(Math.max(config.navCount || 5, 3), 9);
const selectedPages = pool.slice(0, navCount);
const isVite = config.architecture === 'vite-react';

// 1. Generate lib/nav.ts (or src/lib/nav.ts for Vite)
const navDir = isVite
? path.join(projectPath, 'src', 'lib')
: path.join(projectPath, 'lib');

if (!fs.existsSync(navDir)) fs.mkdirSync(navDir, { recursive: true });
fs.writeFileSync(path.join(navDir, 'nav.ts'), generateNavTs(selectedPages));

// 2. Generate route/page files for pages that don't already exist
for (const page of selectedPages) {
if (!page.slug) continue; // Skip home page — already exists as page.tsx

if (isVite) {
const pagePath = path.join(projectPath, 'src', 'pages', `${page.label.replace(/\s+/g, '')}.tsx`);
if (!fs.existsSync(pagePath)) {
const dir = path.dirname(pagePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(pagePath, generateVitePageComponent(page, config));
}
} else {
const routeDir = path.join(projectPath, 'app', page.slug);
const routeFile = path.join(routeDir, 'page.tsx');
if (!fs.existsSync(routeFile)) {
if (!fs.existsSync(routeDir)) fs.mkdirSync(routeDir, { recursive: true });
fs.writeFileSync(routeFile, generatePageComponent(page, config));
}
}
}

return selectedPages;
}
16 changes: 4 additions & 12 deletions templates/blog/nextjs-monolith/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
import { navLinks } from '../lib/nav';
import AnimationProvider from '../components/AnimationProvider';
{{#if (eq design "Dark Terminal")}}
import { Terminal } from 'lucide-react';
Expand All @@ -11,13 +12,6 @@ export const metadata: Metadata = {
description: 'A {{variant}} blog built with Opusify CLI.',
};

const navLinks = [
{ label: 'Home', href: '/' },
{ label: 'Articles', href: '/articles' },
{ label: 'Categories', href: '/categories' },
{ label: 'Authors', href: '/authors' },
];

function Navbar() {
return (
<header className="border-b border-border bg-card">
Expand All @@ -40,11 +34,9 @@ function Navbar() {
</li>
))}
</ul>
<div>
<button className="px-4 py-2 rounded-theme bg-primary text-white text-sm font-medium hover:bg-primary-hover transition">
Subscribe
</button>
</div>
<button className="px-4 py-2 rounded-theme bg-primary text-white text-sm font-medium hover:bg-primary-hover transition">
Subscribe
</button>
</nav>
</header>
);
Expand Down
8 changes: 1 addition & 7 deletions templates/ecommerce/nextjs-monolith/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
import { navLinks } from '../lib/nav';
import AnimationProvider from '../components/AnimationProvider';
{{#if (eq design "Dark Terminal")}}
import { Terminal } from 'lucide-react';
Expand All @@ -11,13 +12,6 @@ export const metadata: Metadata = {
description: 'A {{variant}} e-commerce store built with Opusify CLI.',
};

const navLinks = [
{ label: 'Home', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Cart', href: '/cart' },
{ label: 'Account', href: '/account' },
];

function Navbar() {
return (
<header className="border-b border-border bg-card">
Expand Down
36 changes: 30 additions & 6 deletions templates/portfolio/nextjs-monolith/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
import { navLinks } from '../lib/nav';
import AnimationProvider from '../components/AnimationProvider';
{{#if (eq design "Dark Terminal")}}
import { Terminal } from 'lucide-react';
Expand All @@ -10,6 +12,33 @@ export const metadata: Metadata = {
description: 'A {{variant}} portfolio built with Opusify CLI.',
};

function Navbar() {
return (
<header className="border-b border-border bg-card">
<nav className="max-w-5xl mx-auto px-6 h-16 flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-primary flex items-center gap-2">
{{#if (eq design "Dark Terminal")}}
<Terminal className="w-5 h-5" />
{{/if}}
{{projectName}}
</Link>
<ul className="flex gap-6">
{navLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-text-secondary hover:text-foreground transition"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
</header>
);
}

export default function RootLayout({
children,
}: {
Expand All @@ -18,12 +47,7 @@ export default function RootLayout({
return (
<html lang="en" data-theme="{{design}}">
<body>
{{#if (eq design "Dark Terminal")}}
<header className="border-b border-border bg-card px-6 py-3 flex items-center gap-2">
<Terminal className="w-5 h-5 text-primary" />
<span className="font-mono text-sm text-primary">{{projectName}}</span>
</header>
{{/if}}
<Navbar />
<AnimationProvider>
{children}
</AnimationProvider>
Expand Down
15 changes: 3 additions & 12 deletions templates/saas/nextjs-monolith/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
import { navLinks } from '../lib/nav';
import AnimationProvider from '../components/AnimationProvider';
{{#if (eq design "Dark Terminal")}}
import { Terminal } from 'lucide-react';
Expand All @@ -11,14 +12,6 @@ export const metadata: Metadata = {
description: 'A {{variant}} SaaS dashboard built with Opusify CLI.',
};

const navLinks = [
{ label: 'Dashboard', href: '/' },
{ label: 'Analytics', href: '/analytics' },
{ label: 'Users', href: '/users' },
{ label: 'Billing', href: '/billing' },
{ label: 'Settings', href: '/settings' },
];

function Navbar() {
return (
<header className="border-b border-border bg-card">
Expand All @@ -41,10 +34,8 @@ function Navbar() {
</li>
))}
</ul>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
A
</div>
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
A
</div>
</nav>
</header>
Expand Down
9 changes: 1 addition & 8 deletions templates/saas/vite-react/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { Link, Outlet } from 'react-router-dom';

const navLinks = [
{ label: 'Dashboard', href: '/' },
{ label: 'Analytics', href: '/analytics' },
{ label: 'Users', href: '/users' },
{ label: 'Billing', href: '/billing' },
{ label: 'Settings', href: '/settings' },
];
import { navLinks } from '../lib/nav';

function Navbar() {
return (
Expand Down
10 changes: 1 addition & 9 deletions templates/school/nextjs-monolith/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
import { navLinks } from '../lib/nav';
import AnimationProvider from '../components/AnimationProvider';
{{#if (eq design "Dark Terminal")}}
import { Terminal } from 'lucide-react';
Expand All @@ -11,15 +12,6 @@ export const metadata: Metadata = {
description: 'A {{variant}} school management system built with Opusify CLI.',
};

const navLinks = [
{ label: 'Dashboard', href: '/' },
{ label: 'Students', href: '/students' },
{ label: 'Courses', href: '/courses' },
{ label: 'Grades', href: '/grades' },
{ label: 'Attendance', href: '/attendance' },
{ label: 'Schedule', href: '/schedule' },
];

function Navbar() {
return (
<header className="border-b border-border bg-card">
Expand Down
Loading