diff --git a/apps/web/app/(site)/about/opengraph-image.tsx b/apps/web/app/(site)/about/opengraph-image.tsx deleted file mode 100644 index daf6e71..0000000 --- a/apps/web/app/(site)/about/opengraph-image.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ImageResponse } from "next/og"; -import { OGImageTemplate } from "../../../src/components/OGImageTemplate"; -import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper"; - -export const size = OG_IMAGE_SIZE; -export const contentType = "image/png"; -export const runtime = "nodejs"; - -export default async function Image() { - const fonts = await getOgFonts(); - - return new ImageResponse( - , - { - ...OG_IMAGE_SIZE, - fonts: fonts as any, - }, - ); -} diff --git a/apps/web/app/(site)/about/page.tsx b/apps/web/app/(site)/about/page.tsx index def8a60..fbaaf18 100644 --- a/apps/web/app/(site)/about/page.tsx +++ b/apps/web/app/(site)/about/page.tsx @@ -1,230 +1,71 @@ -"use client"; - -import { Reveal } from "@/src/components/Reveal"; -import { Section } from "@/src/components/Section"; -import { Signature } from "@/src/components/Signature"; +import type { Metadata } from "next"; +import { Container, SectionHeader } from "@/src/components/site"; +import { Timeline } from "@/src/components/about/Timeline"; +import { TagList } from "@/src/components/about/TagList"; import { - H1, - H3, - LeadText, - BodyText, - Label, - MonoLabel, -} from "@/src/components/Typography"; -import { Button } from "@/src/components/Button"; -import { AbstractCircuit, GradientMesh } from "@/src/components/Effects"; -import { GlitchText } from "@/src/components/GlitchText"; + NOT_MY_THING, + STACK_ITEMS, + TIMELINE, + WORKING_MODE, +} from "@/src/components/about/aboutData"; -export default function AboutPage() { +export const metadata: Metadata = { + title: "About", + description: + "Senior engineer who makes AI-built software dependable. Web since 2011, remote since 2021, now independent.", + alternates: { canonical: "/about" }, +}; + +// TODO(owner): link a CV download here once the PDF exists (no file yet). + +export default function Page() { return ( -
- - {/* Hero Section */} -
-
-
- -
- Marc Mintel -
-
+ + -
- - - WEBENTWICKLER - - - -

- Über mich. -

-
- -

- 15 Jahre Erfahrung. Ein Ziel: Ihnen den Frust mit Websites zu - ersparen. -

-
-
-
-
+
+

+ I have worked on the web since 2011, starting with an apprenticeship. + Since then I have worked in agencies and product companies, and led + the frontend team at an agency. I have been fully remote since 2021 + and am now independent, while running a small product business. +

+

+ Design and frontend are my strength. I am comfortable with backend + concepts and architecture. Today I mostly write TypeScript and + Next.js, plus Rust and Solidity for systems work. The path started + with Vue and plain JavaScript, and I would rather say so than pretend + I always wrote Rust. +

+
+ +
+

+ How I work +

+
    + {WORKING_MODE.map((line) => ( +
  • {line}
  • + ))} +
- {/* Section 01: Story */} -
-
- -

- Aus der Praxis.
- Für die Praxis. -

-
+
+

+ Timeline +

+ +
-
- - - Ich habe in Agenturen, Konzernen und Startups gearbeitet. Dabei - habe ich gesehen, woran Projekte scheitern: zu viele Leute, zu - viel Blabla. - - - -
- - Das mache ich anders. Keine Hierarchien, kein Fachchinesisch, - sondern klare Kommunikation. Fokus auf Ergebnisse statt auf - endlose Meetings. - -
-
- - - Sie sprechen immer mit mir. - -
-
- - - Wir beschränken uns auf das Wesentliche. - -
-
-
-
-
-
-
- - {/* Section 02: Ablauf */} -
-
- -

- So einfach läuft
- ein Projekt ab. -

-
- -
- {[ - { - step: "01", - title: "Kurzes Gespräch", - desc: "Kein Workshop, nur die Fakten.", - }, - { - step: "02", - title: "Klares Angebot", - desc: "Fixpreis ohne Sternchen.", - }, - { - step: "03", - title: "Umsetzung", - desc: "Ich baue, Sie entspannen.", - }, - { - step: "04", - title: "Go-Live & Support", - desc: "Ich übernehme die Wartung.", - }, - ].map((item, i) => ( - -
- -
- - - {item.desc} - -
-
-
- ))} -
-
-
- - {/* Section 03: Garantie */} -
-
- -

- Ich stehe für
- meine Arbeit gerade. -

-
- -
- -
- - Wenn etwas nicht passt, liegt die Verantwortung allein bei - mir. - - - Sie beauftragen mich, damit Sie Ruhe haben. Fixpreis bedeutet - Fixpreis. Wir gehen erst live, wenn Sie zu 100% zufrieden - sind. - -
-
- -
- -
-
-
-
-
- - {/* Section 04: CTA */} -
} - > -
-
- -

- Bereit, das Thema
- - - abzuhaken? - - -

-
- - -
- - Schreiben Sie mir einfach – wir finden die beste Lösung für - Ihre Website. - - -
-
-
-
-
-
+
+ + +
+ ); } diff --git a/apps/web/app/(site)/book/page.tsx b/apps/web/app/(site)/book/page.tsx index de7fa0a..b7c6ba3 100644 --- a/apps/web/app/(site)/book/page.tsx +++ b/apps/web/app/(site)/book/page.tsx @@ -1,8 +1,20 @@ import type { Metadata } from "next"; -import { Container } from "@/src/components/site/Container"; -import { SectionHeader } from "@/src/components/site/SectionHeader"; +import { Container, SectionHeader } from "@/src/components/site"; +import { AvailabilityLine } from "@/src/components/book/AvailabilityLine"; +import { BookingEmbed } from "@/src/components/book/BookingEmbed"; -export const metadata: Metadata = { title: "Book a call" }; +export const metadata: Metadata = { + title: "Book a call", + description: + "Book a 20-minute intro call to talk about your project, team and timeline.", + alternates: { canonical: "/book" }, +}; + +const PREPARE: readonly string[] = [ + "What you are building", + "Your team size", + "A rough timeline", +]; export default function Page() { return ( @@ -11,8 +23,26 @@ export default function Page() { as="h1" kicker="/book" title="Book a call" - lead="This page is coming soon." + lead="20 minutes to check whether we fit: your project, where it is stuck or unproven, and how a weekly capacity block would work." /> +
+ +
+ +
+

+ What to prepare +

+ +
+ +
+ +
); } diff --git a/apps/web/src/components/about/TagList.tsx b/apps/web/src/components/about/TagList.tsx new file mode 100644 index 0000000..84fd23a --- /dev/null +++ b/apps/web/src/components/about/TagList.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +type TagListProps = { + readonly title: string; + readonly items: readonly string[]; +}; + +export const TagList: React.FC = ({ title, items }) => ( +
+

{title}

+
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+
+); diff --git a/apps/web/src/components/about/Timeline.tsx b/apps/web/src/components/about/Timeline.tsx new file mode 100644 index 0000000..f28eb53 --- /dev/null +++ b/apps/web/src/components/about/Timeline.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import type { TimelineEntry } from "./aboutData"; + +type TimelineProps = { readonly entries: readonly TimelineEntry[] }; + +export const Timeline: React.FC = ({ entries }) => ( +
    + {entries.map((entry) => ( +
  1. + {entry.period} + + {entry.role}, {entry.company} + {entry.note ? ( + ({entry.note}) + ) : null} + +
  2. + ))} +
+); diff --git a/apps/web/src/components/about/aboutData.ts b/apps/web/src/components/about/aboutData.ts new file mode 100644 index 0000000..9d51bf3 --- /dev/null +++ b/apps/web/src/components/about/aboutData.ts @@ -0,0 +1,53 @@ +export type TimelineEntry = { + readonly period: string; + readonly role: string; + readonly company: string; + readonly note?: string; +}; + +export const TIMELINE: readonly TimelineEntry[] = [ + { period: "2011-2014", role: "Apprenticeship", company: "webfactory.de" }, + { period: "2014-2016", role: "Frontend developer", company: "gct" }, + { + period: "2016-2020", + role: "Frontend developer to frontend team lead", + company: "i22 Digitalagentur", + }, + { period: "2020-2021", role: "Senior developer", company: "Trusted Shops" }, + { + period: "2021-2023", + role: "Senior developer", + company: "sdox.io", + note: "remote", + }, + { + period: "2024-now", + role: "Founder", + company: "Cable Creations", + note: "own product business", + }, +]; + +export const STACK_ITEMS: readonly string[] = [ + "TypeScript", + "Next.js / React", + "Tailwind", + "Node", + "Rust", + "Solidity", + "Postgres basics", + "Docker", +]; + +export const NOT_MY_THING: readonly string[] = [ + "Windows apps", + "Mobile apps", + "Running client infrastructure", + "Database tuning", +]; + +export const WORKING_MODE: readonly string[] = [ + "Async-first and fully remote, with overlap hours in the DACH time zone.", + "Transparent time tracking and a weekly delivery report.", + "Simple SOW and a 2-week notice period.", +]; diff --git a/apps/web/src/components/book/AvailabilityLine.tsx b/apps/web/src/components/book/AvailabilityLine.tsx new file mode 100644 index 0000000..9c49d95 --- /dev/null +++ b/apps/web/src/components/book/AvailabilityLine.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; +import { + formatAvailability, + resolveAvailability, +} from "@/src/domain/availability"; +import { AVAILABILITY_CONFIG } from "@/src/content/site"; + +const toIsoDate = (date: Date): string => { + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${date.getFullYear()}-${month}-${day}`; +}; + +export const AvailabilityLine: React.FC = () => { + const [text, setText] = React.useState(null); + + React.useEffect(() => { + setText( + formatAvailability( + resolveAvailability(AVAILABILITY_CONFIG, toIsoDate(new Date())), + ), + ); + }, []); + + if (text === null) return null; + return

{text}

; +}; diff --git a/apps/web/src/components/book/BookingEmbed.tsx b/apps/web/src/components/book/BookingEmbed.tsx new file mode 100644 index 0000000..ba34e03 --- /dev/null +++ b/apps/web/src/components/book/BookingEmbed.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { parseBookingUrl } from "@/src/domain/booking"; +import { BOOKING_URL, CONTACT_EMAIL } from "@/src/content/site"; + +export const BookingEmbed: React.FC = () => { + const parsed = parseBookingUrl(BOOKING_URL); + + if (parsed.ok === false) { + return ( +
+

+ Booking link not configured +

+

+ The calendar is not available yet. Please email{" "} + {CONTACT_EMAIL} to arrange a + call. +

+
+ ); + } + + const { href } = parsed.value; + return ( +
+