diff --git a/app/[id]/page.tsx b/app/[id]/page.tsx
deleted file mode 100644
index 79a1f92..0000000
--- a/app/[id]/page.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
-
-interface Template {
- id: number;
- name: string;
- displayName: string;
- path: string;
- framework: string;
- rating: number;
- features: string[];
- components: string;
- easeOfSetup: number;
- useCase: string;
- tier: number;
- hasScreenshot: boolean;
- description?: string;
- updatedDate?: string;
- setupInstructions?: string[];
-}
-
-// Same templates array from main page (in production, this would come from a shared data file)
-const templates: Template[] = [
- // Tier 1 - Excellent (5 stars)
- {
- id: 1,
- name: 'brainwave-update-May2024',
- displayName: 'Brainwave',
- path: 'brainwave-update-May2024',
- framework: 'Next.js 14.2 + TS',
- rating: 5,
- features: ['AI Chat', 'Audio/Video', 'Photo Edit', 'Pricing'],
- components: '50+ comp',
- easeOfSetup: 5,
- useCase: 'AI SaaS platforms',
- tier: 1,
- hasScreenshot: true,
- description: 'Modern AI SaaS platform with chat, audio/video processing, and photo editing capabilities. Built with Next.js 14.2 and TypeScript for optimal performance.',
- updatedDate: '2024-05-01',
- setupInstructions: [
- 'cd /Users/z/work/hanzo/templates/brainwave-update-May2024',
- 'npm install',
- 'npm run dev',
- 'Open http://localhost:3000'
- ],
- },
- // Add other templates here (abbreviated for brevity)
-];
-
-export default function TemplateDetail() {
- const params = useParams();
- const templateId = parseInt(params.id as string);
- const template = templates.find((t) => t.id === templateId);
-
- if (!template) {
- return (
-
-
-
Template Not Found
-
- Back to Gallery
-
-
-
- );
- }
-
- const copyToClipboard = (text: string) => {
- navigator.clipboard.writeText(text);
- alert('Copied to clipboard!');
- };
-
- return (
-
-
- {/* Back Button */}
-
- ← Back to Gallery
-
-
- {/* Header */}
-
-
-
-
- {template.displayName}
-
-
{template.framework}
-
-
- {[...Array(5)].map((_, i) => (
-
- ★
-
- ))}
-
-
-
- {/* Tags */}
-
-
- Tier {template.tier}
-
-
- {template.components}
-
-
- Setup: {template.easeOfSetup}/5
-
-
-
-
- {/* Main Content Grid */}
-
- {/* Screenshot */}
-
-
- {template.hasScreenshot ? (
-
- ) : (
-
- Screenshot pending
-
- )}
-
-
-
- {/* Info Panel */}
-
- {/* Description */}
-
-
Description
-
- {template.description || 'No description available.'}
-
-
-
- {/* Use Case */}
-
-
Best For
-
{template.useCase}
-
-
- {/* Action Buttons */}
-
-
-
-
-
-
-
- {/* Features */}
-
-
Features
-
- {template.features.map((feature, i) => (
-
- {feature}
-
- ))}
-
-
-
- {/* Setup Instructions */}
- {template.setupInstructions && (
-
-
Setup Instructions
-
- {template.setupInstructions.map((instruction, i) => (
-
-
- {i + 1}
-
-
- {instruction}
-
-
-
- ))}
-
-
- )}
-
- {/* Tech Stack */}
-
-
Tech Stack
-
-
-
Framework
-
{template.framework}
-
-
-
Components
-
{template.components}
-
-
-
Last Updated
-
{template.updatedDate || 'N/A'}
-
-
-
Ease of Setup
-
- {[...Array(5)].map((_, i) => (
-
- ●
-
- ))}
-
-
-
-
-
-
- );
-}
diff --git a/app/api/build-template/route.ts b/app/api/build-template/route.ts
deleted file mode 100644
index f06ff4b..0000000
--- a/app/api/build-template/route.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import fs from 'fs';
-import path from 'path';
-
-const execAsync = promisify(exec);
-
-const TEMPLATES_ROOT = path.resolve(process.cwd(), '../');
-const PREVIEWS_DIR = path.resolve(process.cwd(), 'public/previews');
-
-interface BuildConfig {
- buildCmd: string | null;
- outputDir: string;
- installCmd: string | null;
-}
-
-const BUILD_CONFIGS: Record = {
- 'HTML/Gulp': {
- buildCmd: './node_modules/.bin/gulp build || npx gulp build',
- outputDir: 'build',
- installCmd: 'npm install',
- },
- 'HTML/CSS': {
- buildCmd: null, // Just copy files
- outputDir: '.',
- installCmd: null,
- },
- 'React': {
- buildCmd: 'npm run build',
- outputDir: 'build',
- installCmd: 'npm install --legacy-peer-deps',
- },
- 'Next.js': {
- buildCmd: 'npm run build && npm run export',
- outputDir: 'out',
- installCmd: 'npm install',
- },
-};
-
-function getBuildConfig(framework: string): BuildConfig {
- if (framework.includes('Gulp')) return BUILD_CONFIGS['HTML/Gulp'];
- if (framework.includes('Next')) return BUILD_CONFIGS['Next.js'];
- if (framework.includes('React')) return BUILD_CONFIGS['React'];
- if (framework.includes('HTML')) return BUILD_CONFIGS['HTML/CSS'];
- return BUILD_CONFIGS['HTML/CSS'];
-}
-
-function copyDir(src: string, dest: string) {
- if (!fs.existsSync(dest)) {
- fs.mkdirSync(dest, { recursive: true });
- }
-
- const entries = fs.readdirSync(src, { withFileTypes: true });
-
- for (const entry of entries) {
- const srcPath = path.join(src, entry.name);
- const destPath = path.join(dest, entry.name);
-
- if (entry.isDirectory()) {
- copyDir(srcPath, destPath);
- } else {
- fs.copyFileSync(srcPath, destPath);
- }
- }
-}
-
-export async function POST(request: NextRequest) {
- try {
- const { templateName, templatePath, framework } = await request.json();
-
- if (!templateName || !templatePath || !framework) {
- return NextResponse.json(
- { error: 'Missing required fields' },
- { status: 400 }
- );
- }
-
- const previewPath = path.join(PREVIEWS_DIR, templateName);
-
- // Check if already built
- if (fs.existsSync(previewPath) && fs.existsSync(path.join(previewPath, 'index.html'))) {
- return NextResponse.json({
- success: true,
- cached: true,
- previewUrl: `/previews/${templateName}/index.html`,
- });
- }
-
- const templateFullPath = path.join(TEMPLATES_ROOT, templatePath);
-
- // Check if template path exists
- if (!fs.existsSync(templateFullPath)) {
- return NextResponse.json(
- { error: `Template path not found: ${templatePath}` },
- { status: 404 }
- );
- }
-
- const config = getBuildConfig(framework);
- const buildOutput = path.join(templateFullPath, config.outputDir);
-
- // Check if template is pre-built (build directory exists but no package.json)
- const packageJson = path.join(templateFullPath, 'package.json');
- const isPreBuilt = fs.existsSync(buildOutput) && !fs.existsSync(packageJson);
-
- if (!isPreBuilt) {
- // Install dependencies if needed
- if (config.installCmd && fs.existsSync(packageJson)) {
- try {
- await execAsync(config.installCmd, {
- cwd: templateFullPath,
- timeout: 300000, // 5 minutes
- });
- } catch (error: unknown) {
- const message = error instanceof Error ? error.message : 'Unknown error';
- return NextResponse.json(
- { error: `Dependency installation failed: ${message}` },
- { status: 500 }
- );
- }
- }
-
- // Build the template
- if (config.buildCmd) {
- try {
- await execAsync(config.buildCmd, {
- cwd: templateFullPath,
- timeout: 600000, // 10 minutes
- });
- } catch (error: unknown) {
- const message = error instanceof Error ? error.message : 'Unknown error';
- return NextResponse.json(
- { error: `Build failed: ${message}` },
- { status: 500 }
- );
- }
- }
- }
-
- if (!fs.existsSync(buildOutput)) {
- return NextResponse.json(
- { error: `Build output directory not found: ${config.outputDir}` },
- { status: 500 }
- );
- }
-
- // Remove existing preview if it exists
- if (fs.existsSync(previewPath)) {
- fs.rmSync(previewPath, { recursive: true, force: true });
- }
-
- copyDir(buildOutput, previewPath);
-
- return NextResponse.json({
- success: true,
- cached: false,
- previewUrl: `/previews/${templateName}/index.html`,
- });
-
- } catch (error: unknown) {
- console.error('Build error:', error);
- const message = error instanceof Error ? error.message : 'Build failed';
- return NextResponse.json(
- { error: message },
- { status: 500 }
- );
- }
-}
diff --git a/app/api/download/[template]/route.ts b/app/api/download/[template]/route.ts
deleted file mode 100644
index 27c63d6..0000000
--- a/app/api/download/[template]/route.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { templates } from '../../../templates-data';
-import archiver from 'archiver';
-import fs from 'fs';
-import path from 'path';
-import os from 'os';
-import { DownloadParamsSchema } from '@/app/lib/validation';
-
-interface RouteParams {
- params: Promise<{
- template: string;
- }>;
-}
-
-export async function GET(
- request: NextRequest,
- { params }: RouteParams
-) {
- try {
- const { template: templateName } = await params;
-
- // Validate template identifier
- try {
- DownloadParamsSchema.parse({ template: templateName });
- } catch {
- return NextResponse.json(
- { error: 'Invalid template identifier' },
- { status: 400 }
- );
- }
-
- // Find the template by slug
- const foundTemplate = templates.find(t => t.slug === decodeURIComponent(templateName));
-
- if (!foundTemplate) {
- return NextResponse.json(
- { error: 'Template not found' },
- { status: 404 }
- );
- }
-
- // Template path (absolute)
- // Use environment variable or resolve relative to project root
- const TEMPLATES_ROOT = process.env.TEMPLATES_ROOT || path.resolve(process.cwd(), '../../');
- const templatePath = path.join(TEMPLATES_ROOT, foundTemplate.path);
-
- // Check if path exists
- if (!fs.existsSync(templatePath)) {
- return NextResponse.json(
- { error: 'Template path does not exist', path: templatePath },
- { status: 404 }
- );
- }
-
- // Create a temporary directory for the zip
- // Use environment variable or system temp directory
- const tmpDir = process.env.TEMP_DIR || path.join(os.tmpdir(), 'hanzo-template-downloads');
- if (!fs.existsSync(tmpDir)) {
- fs.mkdirSync(tmpDir, { recursive: true });
- }
-
- // Sanitize filename
- const safeTemplateName = foundTemplate.name.replace(/[^a-zA-Z0-9-_]/g, '-');
- const zipFileName = `${safeTemplateName}-hanzo.zip`;
- const zipPath = path.join(tmpDir, zipFileName);
-
- // Remove old zip if exists
- if (fs.existsSync(zipPath)) {
- fs.unlinkSync(zipPath);
- }
-
- // Create zip using archiver (safer than shell commands)
- const output = fs.createWriteStream(zipPath);
- const archive = archiver('zip', {
- zlib: { level: parseInt(process.env.ZIP_COMPRESSION_LEVEL || '9') }
- });
-
- // Handle events
- output.on('close', () => {
- console.log(`Created zip: ${archive.pointer()} total bytes`);
- });
-
- archive.on('error', (err) => {
- throw err;
- });
-
- // Pipe archive to output file
- archive.pipe(output);
-
- // Add template directory with exclusions
- archive.glob('**/*', {
- cwd: templatePath,
- ignore: [
- 'node_modules/**',
- '.next/**',
- '.git/**',
- 'build/**',
- 'dist/**',
- '**/.DS_Store',
- '**/*.log',
- '**/.env.local',
- '**/.env'
- ]
- });
-
- // Finalize the archive and wait for it to complete
- try {
- await archive.finalize();
- } catch (error) {
- console.error('Zip creation error:', error);
- return NextResponse.json(
- { error: 'Failed to create zip file', details: error instanceof Error ? error.message : 'Unknown error' },
- { status: 500 }
- );
- }
-
- // Wait for the output stream to finish writing
- await new Promise((resolve, reject) => {
- output.on('finish', resolve);
- output.on('error', reject);
- });
-
- // Read the zip file
- const zipBuffer = fs.readFileSync(zipPath);
-
- // Clean up the zip file after a delay
- setTimeout(() => {
- try {
- if (fs.existsSync(zipPath)) {
- fs.unlinkSync(zipPath);
- }
- } catch (error) {
- console.error('Failed to clean up zip file:', error);
- }
- }, 60000); // Clean up after 1 minute
-
- // Return the zip file
- return new NextResponse(zipBuffer, {
- headers: {
- 'Content-Type': 'application/zip',
- 'Content-Disposition': `attachment; filename="${zipFileName}"`,
- 'Content-Length': zipBuffer.length.toString(),
- },
- });
-
- } catch (error) {
- console.error('Download error:', error);
- return NextResponse.json(
- { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
- { status: 500 }
- );
- }
-}
diff --git a/app/api/list-pages/route.ts b/app/api/list-pages/route.ts
deleted file mode 100644
index 24d0dbf..0000000
--- a/app/api/list-pages/route.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import fs from 'fs';
-import path from 'path';
-
-const PREVIEWS_DIR = path.resolve(process.cwd(), 'public/previews');
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const templateName = searchParams.get('template');
-
- if (!templateName) {
- return NextResponse.json(
- { error: 'Template name required' },
- { status: 400 }
- );
- }
-
- const previewPath = path.join(PREVIEWS_DIR, templateName);
-
- // Check if preview exists
- if (!fs.existsSync(previewPath)) {
- return NextResponse.json(
- { error: 'Template not built yet', pages: [] },
- { status: 404 }
- );
- }
-
- try {
- // Find all HTML files recursively
- const htmlFiles: string[] = [];
-
- function findHtmlFiles(dir: string, baseDir: string = dir) {
- const files = fs.readdirSync(dir, { withFileTypes: true });
-
- for (const file of files) {
- const fullPath = path.join(dir, file.name);
-
- if (file.isDirectory()) {
- // Skip common build artifact directories
- if (['node_modules', '.next', 'dist', 'build'].includes(file.name)) {
- continue;
- }
- findHtmlFiles(fullPath, baseDir);
- } else if (file.name.endsWith('.html')) {
- // Get relative path from preview directory
- const relativePath = path.relative(baseDir, fullPath);
- htmlFiles.push(relativePath);
- }
- }
- }
-
- findHtmlFiles(previewPath);
-
- // Sort files: index.html first, then alphabetically
- htmlFiles.sort((a, b) => {
- if (a === 'index.html') return -1;
- if (b === 'index.html') return 1;
- return a.localeCompare(b);
- });
-
- return NextResponse.json({
- success: true,
- pages: htmlFiles.length > 0 ? htmlFiles : ['index.html'],
- count: htmlFiles.length,
- });
-
- } catch (error: unknown) {
- const message = error instanceof Error ? error.message : 'Unknown error';
- return NextResponse.json(
- { error: message, pages: [] },
- { status: 500 }
- );
- }
-}
diff --git a/app/templates/[slug]/page.tsx b/app/templates/[slug]/page.tsx
index 87fbac6..f7fb8ec 100644
--- a/app/templates/[slug]/page.tsx
+++ b/app/templates/[slug]/page.tsx
@@ -1,35 +1,43 @@
-import { templates } from '../../templates-data';
-import { notFound } from 'next/navigation';
-import { getTemplateVariants, getUniqueTemplates } from '../../lib/template-utils';
-import { TemplatePageClient } from './TemplatePageClient';
-
-interface PageProps {
- params: Promise<{ slug: string }>;
-}
-
-export default async function TemplatePage({ params }: PageProps) {
- const { slug } = await params;
- const allVariants = getTemplateVariants(templates, slug);
-
- if (allVariants.length === 0) {
- notFound();
- }
-
- // Get unique templates for navigation
- const uniqueTemplates = getUniqueTemplates(templates);
- const currentIndex = uniqueTemplates.findIndex(t => t.slug === slug);
-
- const prevTemplate = currentIndex > 0 ? uniqueTemplates[currentIndex - 1] : null;
- const nextTemplate = currentIndex < uniqueTemplates.length - 1 ? uniqueTemplates[currentIndex + 1] : null;
-
- return (
-
- );
-}
+import { templates } from '../../templates-data';
+import { notFound } from 'next/navigation';
+import { getTemplateVariants, getUniqueTemplates } from '../../lib/template-utils';
+import { TemplatePageClient } from './TemplatePageClient';
+
+interface PageProps {
+ params: Promise<{ slug: string }>;
+}
+
+// Generate static params for all templates at build time
+export async function generateStaticParams() {
+ const uniqueTemplates = getUniqueTemplates(templates);
+ return uniqueTemplates.map((template) => ({
+ slug: template.slug,
+ }));
+}
+
+export default async function TemplatePage({ params }: PageProps) {
+ const { slug } = await params;
+ const allVariants = getTemplateVariants(templates, slug);
+
+ if (allVariants.length === 0) {
+ notFound();
+ }
+
+ // Get unique templates for navigation
+ const uniqueTemplates = getUniqueTemplates(templates);
+ const currentIndex = uniqueTemplates.findIndex(t => t.slug === slug);
+
+ const prevTemplate = currentIndex > 0 ? uniqueTemplates[currentIndex - 1] : null;
+ const nextTemplate = currentIndex < uniqueTemplates.length - 1 ? uniqueTemplates[currentIndex + 1] : null;
+
+ return (
+
+ );
+}
diff --git a/next.config.ts b/next.config.ts
index 1f6f6ce..afcbe7e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
+ output: 'export',
images: {
unoptimized: true,
},