201 lines
7.1 KiB
JavaScript
201 lines
7.1 KiB
JavaScript
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execSync } = require('node:child_process');
|
|
|
|
const SRC_DIR = path.join(__dirname, '../src');
|
|
const DIST_DIR = path.join(__dirname, '../dist');
|
|
const APPS_DIR = path.join(SRC_DIR, 'apps');
|
|
|
|
// Standard Navigation Template
|
|
// Note: We use absolute paths (/) so they work from any depth, assuming Cloudflare Pages root.
|
|
const NAV_TEMPLATE = `
|
|
<nav class="bg-white/70 backdrop-blur-sm border-b-2 border-amber-800 p-3 sticky top-0 z-20">
|
|
<div class="max-w-6xl mx-auto flex justify-between items-center font-impact text-amber-900">
|
|
<a href="/" class="text-2xl font-black hover:text-amber-700 transition-colors">cffyin</a>
|
|
<div class="flex gap-6 text-lg items-center">
|
|
<a href="/" class="hover:underline">Home</a>
|
|
<a href="/german-trainer/" class="hover:underline">German Trainer</a>
|
|
<a href="/hexarow/" class="hover:underline">Hex-a-Row</a>
|
|
<a href="/snapmark/" class="hover:underline">SnapMark</a>
|
|
<a href="/bridge-builder-math/" class="hover:underline">Bridge Builder</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
`;
|
|
|
|
function cleanDist() {
|
|
if (fs.existsSync(DIST_DIR)) {
|
|
fs.rmSync(DIST_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(DIST_DIR);
|
|
}
|
|
|
|
function processHtml(filePath, outPath) {
|
|
let content = fs.readFileSync(filePath, 'utf-8');
|
|
|
|
// Simple regex-based injection.
|
|
// 1. Remove existing <nav>...</nav> (if any) to avoid duplication.
|
|
// 2. Inject new NAV_TEMPLATE at the start of <body>.
|
|
|
|
// Remove existing nav
|
|
content = content.replace(/<nav[\s\S]*?<\/nav>/i, '');
|
|
|
|
// Inject new nav
|
|
if (content.includes('<body')) {
|
|
content = content.replace(/<body([^>]*)>/i, `<body>\n${NAV_TEMPLATE}`);
|
|
} else {
|
|
// Fallback if no body tag found (unlikely for valid HTML)
|
|
content = NAV_TEMPLATE + content;
|
|
}
|
|
|
|
// Inject Apps Grid if placeholder exists
|
|
if (content.includes('<!-- APPS_GRID -->')) {
|
|
const appsGridHtml = generateAppsGridHtml();
|
|
content = content.replace('<!-- APPS_GRID -->', appsGridHtml);
|
|
}
|
|
|
|
fs.writeFileSync(outPath, content);
|
|
console.log(`Processed ${filePath} -> ${outPath}`);
|
|
}
|
|
|
|
function generateAppsGridHtml() {
|
|
if (!fs.existsSync(APPS_DIR)) return '';
|
|
|
|
const apps = fs.readdirSync(APPS_DIR).filter(item => {
|
|
const itemPath = path.join(APPS_DIR, item);
|
|
return !item.startsWith('.') && fs.statSync(itemPath).isDirectory();
|
|
});
|
|
|
|
return apps.map(app => {
|
|
const metadataPath = path.join(APPS_DIR, app, 'metadata.json');
|
|
let metadata = {
|
|
title: app,
|
|
description: 'A cool app.',
|
|
emoji: '📱',
|
|
color: 'gray',
|
|
path: `/${app}/`
|
|
};
|
|
|
|
if (fs.existsSync(metadataPath)) {
|
|
try {
|
|
const data = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
metadata = { ...metadata, ...data };
|
|
} catch (e) {
|
|
console.error(`Error reading metadata for ${app}:`, e);
|
|
}
|
|
}
|
|
|
|
return `
|
|
<div class="group">
|
|
<a href="${metadata.path}" class="block bg-${metadata.color}-400 border-4 border-${metadata.color}-600 shadow-lg p-6 space-y-4 transition-all duration-300 transform rotate-1 group-hover:rotate-0 group-hover:scale-105">
|
|
<div class="text-center">
|
|
<div class="text-6xl mb-3">${metadata.emoji}</div>
|
|
<h4 class="font-impact text-3xl font-black text-gray-900 mb-2">${metadata.title}</h4>
|
|
<p class="text-gray-800 font-bold text-lg">${metadata.description}</p>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<div class="px-4 py-2 text-sm font-black border-2 bg-green-400 border-green-700 text-green-900">ONLINE</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
`;
|
|
}).join('\n');
|
|
}
|
|
|
|
|
|
function buildLanding() {
|
|
const landingSrc = path.join(SRC_DIR, 'landing', 'index.html');
|
|
const landingDist = path.join(DIST_DIR, 'index.html');
|
|
if (fs.existsSync(landingSrc)) {
|
|
processHtml(landingSrc, landingDist);
|
|
} else {
|
|
console.error("Landing page not found!");
|
|
}
|
|
}
|
|
|
|
function buildApps() {
|
|
if (!fs.existsSync(APPS_DIR)) return;
|
|
|
|
const apps = fs.readdirSync(APPS_DIR).filter(item => {
|
|
const itemPath = path.join(APPS_DIR, item);
|
|
return !item.startsWith('.') && fs.statSync(itemPath).isDirectory();
|
|
});
|
|
|
|
apps.forEach(app => {
|
|
const appSrc = path.join(APPS_DIR, app);
|
|
const appDist = path.join(DIST_DIR, app);
|
|
|
|
fs.mkdirSync(appDist, { recursive: true });
|
|
|
|
// Check if it's a Vite app (has package.json and vite.config.ts)
|
|
if (fs.existsSync(path.join(appSrc, 'package.json')) && fs.existsSync(path.join(appSrc, 'vite.config.ts'))) {
|
|
console.log(`Building Vite app: ${app}...`);
|
|
try {
|
|
// Install dependencies if needed
|
|
if (!fs.existsSync(path.join(appSrc, 'node_modules'))) {
|
|
console.log(`Installing dependencies for ${app}...`);
|
|
execSync('npm install', { cwd: appSrc, stdio: 'inherit' });
|
|
}
|
|
|
|
// Build
|
|
execSync('npm run build', { cwd: appSrc, stdio: 'inherit' });
|
|
|
|
// Copy dist contents
|
|
const viteDist = path.join(appSrc, 'dist');
|
|
if (fs.existsSync(viteDist)) {
|
|
fs.cpSync(viteDist, appDist, { recursive: true });
|
|
} else {
|
|
console.error(`Vite build failed to produce 'dist' for ${app}`);
|
|
}
|
|
} catch (e) {
|
|
console.error(`Failed to build ${app}:`, e);
|
|
}
|
|
} else {
|
|
// Standard HTML app
|
|
console.log(`Processing HTML app: ${app}...`);
|
|
const files = fs.readdirSync(appSrc);
|
|
files.forEach(file => {
|
|
const srcFile = path.join(appSrc, file);
|
|
const distFile = path.join(appDist, file);
|
|
|
|
if (file.endsWith('.html')) {
|
|
processHtml(srcFile, distFile);
|
|
} else {
|
|
fs.cpSync(srcFile, distFile, { recursive: true });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function createRedirect(filename, target) {
|
|
const content = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="refresh" content="0; url=${target}" />
|
|
<script>window.location.href = "${target}";</script>
|
|
<title>Redirecting...</title>
|
|
</head>
|
|
<body>
|
|
<p>If you are not redirected automatically, follow this <a href="${target}">link</a>.</p>
|
|
</body>
|
|
</html>`;
|
|
const filePath = path.join(DIST_DIR, filename);
|
|
fs.writeFileSync(filePath, content);
|
|
console.log(`Created static redirect: ${filename} -> ${target}`);
|
|
}
|
|
|
|
function main() {
|
|
console.log("Starting Build...");
|
|
cleanDist();
|
|
buildLanding();
|
|
buildApps();
|
|
|
|
// Create static redirects for legacy paths
|
|
createRedirect('game.html', '/hexarow/');
|
|
createRedirect('german.html', '/german-trainer/');
|
|
|
|
console.log("Build Complete!");
|
|
}
|
|
|
|
main(); |