feat: rebuild mintel.me as a single-page freelance site

The site now presents one freelance developer on one page: hero, introduction,
four typical cases (developer for a product, make an app reliable, automate a
process, a website) with what you get, how it runs and what it costs, a
process automation section, technology and project fit, how I work, two live
references, questions and contact.

- Hero and closing headline are drawn from running binary digits.
- Each case explains its four steps with an animated drawing; the automation
  section shows example workflows with work travelling between systems; a
  pipeline run shows what a test catches before a release.
- Contact is one email address, kept in parts and assembled in the browser so
  it never appears in the page source (src/domain/email.ts, spec first).
- Imprint and privacy pages added.
- The old site (blog, case studies, contact form, fixed-price pages, video
  rendering and related scripts) is removed. Its URLs redirect to the homepage.
- The deploy smoke test checks the legal pages and the redirects instead of
  the removed case study.
- Dev scripts use docker compose v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 22:33:06 +02:00
co-authored by Claude Opus 5.5
parent 4eb663f42a
commit fa40fefe2f
781 changed files with 4925 additions and 55137 deletions
+47 -155
View File
@@ -1,175 +1,67 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { useSafePathname } from "./analytics/useSafePathname";
import * as React from "react";
import { ButtonLink } from "./site/ButtonLink";
import IconWhite from "../assets/logo/Icon-White-Transparent.svg";
/** Header: the logo and one action. Everything else lives on the homepage. */
export const Header: React.FC = () => {
const pathname = useSafePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
// Close mobile menu on pathname change and handle body scroll lock
React.useEffect(() => {
setIsMobileMenuOpen(false);
}, [pathname]);
const [isScrolled, setIsScrolled] = React.useState(false);
React.useEffect(() => {
if (isMobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
}, [isMobileMenuOpen]);
const navLinks = [
{ href: "/notes", label: "Notes" },
{ href: "/process", label: "Process" },
{ href: "/about", label: "About" },
] as const;
const linkActive = (href: string) =>
pathname === href || Boolean(pathname?.startsWith(`${href}/`));
const closeMenu = () => setIsMobileMenuOpen(false);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<header className="sticky top-0 z-[100] w-full border-b border-border bg-bg">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-5 py-4 md:px-8">
<header className="sticky top-0 z-[100] w-full">
{/* Decoupled Background Layer - Prevents backdrop-filter parent context bugs */}
<div
className={`absolute inset-0 transition-all duration-500 -z-10 ${
isScrolled
? "bg-white/70 backdrop-blur-xl border-b border-slate-100 shadow-sm shadow-slate-100/50"
: "bg-white/80 backdrop-blur-md border-b border-slate-50"
}`}
/>
<div className="mx-auto w-full max-w-[1400px] px-5 md:px-12 py-4 flex items-center justify-between relative z-10">
<Link
href="/"
aria-label="mintel.me home"
className="font-mono text-lg font-semibold text-fg"
aria-label="Marc Mintel, back to top"
onClick={(event) => {
// Already on the homepage: scroll up instead of reloading the route.
if (window.location.pathname === "/") {
event.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
}}
className="flex items-center gap-4 group"
>
mintel<span className="text-accent">.</span>me
</Link>
{/* Desktop Navigation */}
<nav aria-label="Main" className="hidden items-center gap-8 md:flex">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
aria-current={linkActive(link.href) ? "page" : undefined}
className={`text-sm transition-colors ${
linkActive(link.href) ? "text-fg" : "text-muted hover:text-fg"
}`}
>
{link.label}
</Link>
))}
<ButtonLink href="/book">Book a call</ButtonLink>
</nav>
{/* Mobile Toggle */}
<button
className="relative z-[110] flex h-10 w-10 items-center justify-center rounded-md border border-border-strong text-fg md:hidden"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle Menu"
aria-expanded={isMobileMenuOpen}
>
<div className="relative flex h-3.5 w-5 flex-col justify-between">
<motion.span
animate={
isMobileMenuOpen ? { rotate: 45, y: 7 } : { rotate: 0, y: 0 }
}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="h-0.5 w-full origin-center rounded-full bg-current"
/>
<motion.span
animate={
isMobileMenuOpen ? { opacity: 0, x: -10 } : { opacity: 1, x: 0 }
}
transition={{ duration: 0.2 }}
className="h-0.5 w-full rounded-full bg-current"
/>
<motion.span
animate={
isMobileMenuOpen ? { rotate: -45, y: -7 } : { rotate: 0, y: 0 }
}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="h-0.5 w-full origin-center rounded-full bg-current"
<div className="w-10 h-10 md:w-12 md:h-12 bg-black rounded-xl flex items-center justify-center group-hover:scale-105 transition-all duration-500 shadow-sm shrink-0 relative overflow-hidden">
<Image
src={IconWhite}
alt=""
width={32}
height={32}
className="w-6 h-6 md:w-8 md:h-8 relative z-10"
/>
</div>
</button>
</Link>
<Link
href="/#contact"
className="font-sans text-[10px] font-bold uppercase tracking-[0.2em] text-slate-900 border border-slate-200 px-5 py-2.5 rounded-full no-underline hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100"
style={{
transitionTimingFunction: "cubic-bezier(0.23, 1, 0.32, 1)",
}}
>
Get in touch
</Link>
</div>
{/* Mobile Navigation - bottom sheet */}
<AnimatePresence>
{isMobileMenuOpen && (
<React.Fragment key="mobile-menu">
<motion.div
key="menu-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
onClick={closeMenu}
className="fixed inset-0 z-[101] bg-black/60 md:hidden"
/>
<motion.div
key="menu-sheet"
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{
type: "spring",
stiffness: 350,
damping: 30,
mass: 0.8,
}}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.15}
onDragEnd={(_, info) => {
if (info.offset.y > 80 || info.velocity.y > 300) {
closeMenu();
}
}}
className="fixed inset-x-0 bottom-0 z-[102] flex max-h-[85vh] flex-col overflow-hidden rounded-t-xl border-t border-border-strong bg-surface md:hidden"
>
<div className="flex cursor-grab justify-center pb-2 pt-3 active:cursor-grabbing">
<div className="h-1 w-10 rounded-full bg-border-strong" />
</div>
<nav
aria-label="Mobile"
className="flex-1 overflow-y-auto px-5 pb-3 pt-2"
>
<ul className="m-0 list-none p-0">
{navLinks.map((item) => (
<li key={item.href} className="!mb-0">
<Link
href={item.href}
onClick={closeMenu}
aria-current={
linkActive(item.href) ? "page" : undefined
}
className={`flex items-center justify-between border-b border-border py-4 text-lg ${
linkActive(item.href) ? "text-accent" : "text-fg"
}`}
>
{item.label}
</Link>
</li>
))}
</ul>
</nav>
<div className="px-5 pb-5 pt-2">
<ButtonLink href="/book" className="w-full">
Book a call
</ButtonLink>
</div>
<div className="h-[env(safe-area-inset-bottom,0px)]" />
</motion.div>
</React.Fragment>
)}
</AnimatePresence>
</header>
);
};