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,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}`);
|
||||
Reference in New Issue
Block a user