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:
Leon-in
2026-04-26 18:19:56 +08:00
commit 95eb362bfc
134 changed files with 25831 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/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 INTERNAL_PREFIXES = ["/", "#"];
const EXTERNAL_DOMAINS = ["http://", "https://", "mailto:", "tel:"];
const TSX_FILES = [];
function walkDir(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
walkDir(full);
} else if (entry.name.endsWith(".tsx")) {
TSX_FILES.push(full);
}
}
}
walkDir(path.join(ROOT, "components"));
let totalConverted = 0;
for (const file of TSX_FILES) {
// Skip Header.tsx - already uses Link
if (file.endsWith("Header.tsx")) continue;
let content = fs.readFileSync(file, "utf8");
let changed = false;
// Convert <a href="..." ...> to <Link href="..." ...> for internal links
// And add target/rel for external links
content = content.replace(/<a\s([^>]*?)>/g, (match, attrs) => {
const hrefMatch = attrs.match(/href="([^"]*)"/);
if (!hrefMatch) return match;
const href = hrefMatch[1];
const isExternal = EXTERNAL_DOMAINS.some(d => href.startsWith(d));
const isInternal = INTERNAL_PREFIXES.some(p => href.startsWith(p)) && !isExternal;
if (isInternal) {
changed = true;
totalConverted++;
// Remove Webflow-specific attrs
let cleanAttrs = attrs
.replace(/data-w-id="[^"]*"/g, "")
.replace(/data-wf[^=]*="[^"]*"/g, "")
.replace(/aria-current="page"/g, "")
.replace(/\s+/g, " ")
.trim();
return `<Link ${cleanAttrs}>`;
}
if (isExternal && !attrs.includes("target=")) {
changed = true;
totalConverted++;
return `<a ${attrs} target="_blank" rel="noopener noreferrer">`;
}
return match;
});
// Convert closing </a> to </Link> where we converted opening tag
// Simple approach: replace all </a> in files that had changes
// Actually we need to be smarter - only replace </a> that correspond to <Link>
// Let's do it line by line
if (changed) {
// Count Link opens vs closes to fix
const lines = content.split("\n");
let linkDepth = 0;
const newLines = [];
for (const line of lines) {
let processed = line;
// Count Link opens in this line
const linkOpens = (processed.match(/<Link\s/g) || []).length;
const selfClosing = (processed.match(/<Link\s[^>]*\/>/g) || []).length;
linkDepth += linkOpens - selfClosing;
// Replace </a> with </Link> when inside a Link context
if (linkDepth > 0 && processed.includes("</a>")) {
const closeTags = (processed.match(/<\/a>/g) || []).length;
for (let i = 0; i < closeTags && linkDepth > 0; i++) {
processed = processed.replace("</a>", "</Link>");
linkDepth--;
}
}
newLines.push(processed);
}
content = newLines.join("\n");
// Add Link import
if (!content.includes('import Link from "next/link"') && !content.includes("import Link from 'next/link'")) {
if (content.includes('import Image from "next/image"')) {
content = content.replace(
'import Image from "next/image"',
'import Image from "next/image"\nimport Link from "next/link"'
);
} else {
const firstLine = content.indexOf("\n");
content = content.slice(0, firstLine + 1) + 'import Link from "next/link"\n' + content.slice(firstLine + 1);
}
}
fs.writeFileSync(file, content, "utf8");
console.log(`Updated: ${path.relative(ROOT, file)}`);
}
}
console.log(`\nTotal links converted: ${totalConverted}`);