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 ? ( - {template.displayName} - ) : ( -
- 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, },