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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/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}/`);
|
||||
}
|
||||
Reference in New Issue
Block a user