feat: Convert to fully static site with pre-generated routes
- Remove API routes (not needed for static export) - Remove old [id] dynamic route - Add generateStaticParams() to templates/[slug] route - Enable output: 'export' for static site generation - Pre-generate all 47 pages at build time (40 templates + 7 static pages) - Ready for GitHub Pages deployment
This commit is contained in:
@@ -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 (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Template Not Found</h1>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors inline-block"
|
||||
>
|
||||
Back to Gallery
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
alert('Copied to clipboard!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||||
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
||||
{/* Back Button */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-white/5 border border-white/10 text-white rounded-lg hover:bg-white/10 transition-colors mb-8"
|
||||
>
|
||||
← Back to Gallery
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-5xl font-bold mb-2 bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||
{template.displayName}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-400">{template.framework}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<span key={i} className={`text-2xl ${i < template.rating ? 'text-yellow-400' : 'text-gray-600'}`}>
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
<span className="px-3 py-1 bg-blue-500/20 border border-blue-500/30 rounded-lg text-blue-300 text-sm">
|
||||
Tier {template.tier}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-purple-500/20 border border-purple-500/30 rounded-lg text-purple-300 text-sm">
|
||||
{template.components}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-green-500/20 border border-green-500/30 rounded-lg text-green-300 text-sm">
|
||||
Setup: {template.easeOfSetup}/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Screenshot */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-xl border border-white/10 overflow-hidden">
|
||||
<div className="relative aspect-video">
|
||||
{template.hasScreenshot ? (
|
||||
<Image
|
||||
src={`/screenshots/${template.name}.png`}
|
||||
alt={template.displayName}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
Screenshot pending
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Panel */}
|
||||
<div className="space-y-6">
|
||||
{/* Description */}
|
||||
<div className="bg-white/5 backdrop-blur-lg p-6 rounded-xl border border-white/10">
|
||||
<h2 className="text-2xl font-bold mb-3 text-white">Description</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
{template.description || 'No description available.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Use Case */}
|
||||
<div className="bg-white/5 backdrop-blur-lg p-6 rounded-xl border border-white/10">
|
||||
<h2 className="text-2xl font-bold mb-3 text-white">Best For</h2>
|
||||
<p className="text-gray-300">{template.useCase}</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => copyToClipboard(`/Users/z/work/hanzo/templates/${template.path}`)}
|
||||
className="w-full px-6 py-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium text-lg flex items-center justify-center gap-2"
|
||||
>
|
||||
📋 Copy Template Path
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
alert('Fork on Hanzo feature coming soon! This would deploy the template to Hanzo Platform.');
|
||||
}}
|
||||
className="w-full px-6 py-4 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg hover:from-purple-600 hover:to-pink-600 transition-all font-medium text-lg flex items-center justify-center gap-2"
|
||||
>
|
||||
🚀 Fork on Hanzo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="bg-white/5 backdrop-blur-lg p-6 rounded-xl border border-white/10 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Features</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{template.features.map((feature, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-center text-gray-300 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Setup Instructions */}
|
||||
{template.setupInstructions && (
|
||||
<div className="bg-white/5 backdrop-blur-lg p-6 rounded-xl border border-white/10 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Setup Instructions</h2>
|
||||
<div className="space-y-3">
|
||||
{template.setupInstructions.map((instruction, i) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<span className="flex-shrink-0 w-8 h-8 bg-blue-500/20 border border-blue-500/30 rounded-full flex items-center justify-center text-blue-300 text-sm">
|
||||
{i + 1}
|
||||
</span>
|
||||
<code className="flex-1 px-4 py-2 bg-black/30 rounded-lg text-gray-300 font-mono text-sm">
|
||||
{instruction}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(instruction)}
|
||||
className="flex-shrink-0 px-3 py-2 bg-white/5 border border-white/10 text-white rounded-lg hover:bg-white/10 transition-colors text-sm"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tech Stack */}
|
||||
<div className="bg-white/5 backdrop-blur-lg p-6 rounded-xl border border-white/10">
|
||||
<h2 className="text-2xl font-bold mb-4 text-white">Tech Stack</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-2">Framework</h3>
|
||||
<p className="text-blue-400">{template.framework}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-2">Components</h3>
|
||||
<p className="text-gray-300">{template.components}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-2">Last Updated</h3>
|
||||
<p className="text-gray-300">{template.updatedDate || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-2">Ease of Setup</h3>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<span key={i} className={i < template.easeOfSetup ? 'text-green-400' : 'text-gray-600'}>
|
||||
●
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string, BuildConfig> = {
|
||||
'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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void>((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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<TemplatePageClient
|
||||
variants={allVariants}
|
||||
prevTemplate={prevTemplate}
|
||||
nextTemplate={nextTemplate}
|
||||
currentIndex={currentIndex + 1}
|
||||
totalTemplates={uniqueTemplates.length}
|
||||
allTemplates={uniqueTemplates}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<TemplatePageClient
|
||||
variants={allVariants}
|
||||
prevTemplate={prevTemplate}
|
||||
nextTemplate={nextTemplate}
|
||||
currentIndex={currentIndex + 1}
|
||||
totalTemplates={uniqueTemplates.length}
|
||||
allTemplates={uniqueTemplates}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'export',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user