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:
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export type CareerStation = {
|
||||
readonly years: string;
|
||||
readonly role: string;
|
||||
readonly place: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* CareerLine: the path so far as a line that draws itself, one station after another.
|
||||
*/
|
||||
export const CareerLine: React.FC<{ stations: readonly CareerStation[] }> = ({
|
||||
stations,
|
||||
}) => (
|
||||
<ol className="relative m-0 list-none p-0">
|
||||
<motion.span
|
||||
className="absolute left-[5px] top-2 w-px origin-top bg-slate-900 md:left-[7px]"
|
||||
style={{ bottom: 8 }}
|
||||
initial={{ scaleY: 0 }}
|
||||
whileInView={{ scaleY: 1 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 1.4, ease: "easeOut" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{stations.map((station, index) => (
|
||||
<motion.li
|
||||
key={station.years}
|
||||
className="relative grid grid-cols-1 gap-1 pb-10 pl-8 last:pb-0 md:grid-cols-12 md:gap-8 md:pl-12"
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, margin: "-15%" }}
|
||||
transition={{ duration: 0.4, delay: 0.15 + index * 0.18 }}
|
||||
>
|
||||
<span
|
||||
className={`absolute left-0 top-1.5 h-[11px] w-[11px] rounded-full border-2 border-slate-900 md:h-[15px] md:w-[15px] ${
|
||||
index === stations.length - 1 ? "bg-slate-900" : "bg-white"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500 md:col-span-3 md:pt-1.5">
|
||||
{station.years}
|
||||
</span>
|
||||
<span className="md:col-span-9">
|
||||
<span className="block font-sans text-xl font-bold tracking-tight text-slate-900 md:text-3xl">
|
||||
{station.role}
|
||||
</span>
|
||||
<span className="mt-1 block font-sans text-base text-slate-500 md:text-lg">
|
||||
{station.place}
|
||||
</span>
|
||||
</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
Reference in New Issue
Block a user