95eb362bfc
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>
169 lines
5.1 KiB
JavaScript
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}/`);
|
|
}
|