Files
dalcode-website/scripts/split-sections.mjs
T
Leon-in 95eb362bfc feat: initial commit — Webflow to Next.js conversion
QuantumLab template converted to Next.js 16 + React 19 + TypeScript:
- 8 page routes (home, about, blog, contact, careers, team-members, coming-soon, 404)
- Dynamic routes for blog posts, career positions, and team members
- GSAP animations (marquee, counters, button hovers)
- IntersectionObserver-based scroll reveal (blur-to-clear transitions)
- Dark mode with next-themes
- React Hook Form + Zod contact form
- Framer Motion page transitions
- Lottie animations via lottie-web

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 18:19:56 +08:00

169 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const PAGES = [
{
file: "app/page.tsx",
dir: "components/home",
pageName: "HomePage",
imports: ['import LottiePlayer from "@/components/LottiePlayer"', 'import VideoLightbox from "@/components/VideoLightbox"'],
},
{
file: "app/about/page.tsx",
dir: "components/about",
pageName: "AboutPage",
imports: ['import LottiePlayer from "@/components/LottiePlayer"'],
},
{
file: "app/blog/page.tsx",
dir: "components/blog",
pageName: "BlogPage",
imports: [],
},
{
file: "app/contact/page.tsx",
dir: "components/contact",
pageName: "ContactPage",
imports: ['import LottiePlayer from "@/components/LottiePlayer"'],
},
];
function toPascalCase(str) {
return str
.replace(/[^a-zA-Z0-9]+/g, " ")
.trim()
.split(/\s+/)
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
.join("");
}
function extractSectionName(openTag) {
const classMatch = openTag.match(/className="([^"]*)"/);
if (!classMatch) return "Section";
const cls = classMatch[1];
const meaningful = cls
.split(/\s+/)
.filter((c) => !["section", "section-small", "overflow-hidden", "w-variant-daeb6173-296b-8c16-4f2d-1c7fb3f823cc"].includes(c))
.join(" ");
if (!meaningful) return "Section";
return toPascalCase(meaningful) || "Section";
}
for (const page of PAGES) {
const filePath = path.join(ROOT, page.file);
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
// Find <main> content boundaries
let mainStart = -1;
let mainEnd = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === "<main>") mainStart = i + 1;
if (lines[i].trim() === "</main>") mainEnd = i;
}
if (mainStart === -1 || mainEnd === -1) {
console.log(`Skipping ${page.file}: no <main> found`);
continue;
}
// Find top-level sections (indented at 6 spaces inside main)
const sections = [];
let currentSection = null;
let depth = 0;
const sectionIndent = lines[mainStart]?.search(/\S/) ?? 6;
for (let i = mainStart; i < mainEnd; i++) {
const line = lines[i];
const indent = line.search(/\S/);
if (indent === -1) {
if (currentSection) currentSection.lines.push(line);
continue;
}
if (indent === sectionIndent && line.trim().startsWith("<section") && !currentSection) {
currentSection = { startLine: i, lines: [line], openTag: line.trim() };
} else if (currentSection) {
currentSection.lines.push(line);
if (indent === sectionIndent && line.trim() === "</section>") {
sections.push(currentSection);
currentSection = null;
}
}
}
if (currentSection) sections.push(currentSection);
if (!sections.length) {
console.log(`Skipping ${page.file}: no sections found`);
continue;
}
// Create component directory
const compDir = path.join(ROOT, page.dir);
fs.mkdirSync(compDir, { recursive: true });
const componentNames = [];
const usedNames = new Set();
for (let idx = 0; idx < sections.length; idx++) {
const section = sections[idx];
let baseName = extractSectionName(section.openTag);
if (usedNames.has(baseName)) baseName += (idx + 1);
usedNames.add(baseName);
const compName = baseName + "Section";
componentNames.push(compName);
// Determine needed imports for this section
const sectionContent = section.lines.join("\n");
const neededImports = [];
if (sectionContent.includes("LottiePlayer")) neededImports.push('import LottiePlayer from "@/components/LottiePlayer"');
if (sectionContent.includes("VideoLightbox")) neededImports.push('import VideoLightbox from "@/components/VideoLightbox"');
// Dedent section content
const minIndent = sectionIndent;
const dedented = section.lines.map((l) => {
if (l.trim() === "") return "";
return l.length > minIndent ? " " + l.slice(minIndent) : " " + l.trimStart();
});
const tsx = `${neededImports.length ? neededImports.join("\n") + "\n\n" : ""}export default function ${compName}() {
return (
${dedented.join("\n")}
)
}
`;
const compFile = path.join(compDir, `${compName}.tsx`);
fs.writeFileSync(compFile, tsx, "utf8");
}
// Write index.ts barrel
const barrel = componentNames
.map((n) => `export { default as ${n} } from "./${n}"`)
.join("\n") + "\n";
fs.writeFileSync(path.join(compDir, "index.ts"), barrel, "utf8");
// Rewrite page.tsx
const relDir = page.dir.replace(/^components\//, "@/components/");
const importLine = `import { ${componentNames.join(", ")} } from "${relDir}"`;
const sectionJsx = componentNames.map((n) => ` <${n} />`).join("\n");
const newPage = `${importLine}
export default function ${page.pageName}() {
return (
<main>
${sectionJsx}
</main>
)
}
`;
fs.writeFileSync(filePath, newPage, "utf8");
console.log(`${page.file}: split into ${sections.length} sections → ${page.dir}/`);
}