feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Failing after 1m2s
Build & Deploy / 🏗️ Build (push) Failing after 3m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
import { PageHeader } from "../../src/components/PageHeader";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
@@ -6,7 +8,6 @@ import {
|
|||||||
ExperienceIllustration,
|
ExperienceIllustration,
|
||||||
ResponsibilityIllustration,
|
ResponsibilityIllustration,
|
||||||
ResultIllustration,
|
ResultIllustration,
|
||||||
ConceptSystem,
|
|
||||||
ContactIllustration,
|
ContactIllustration,
|
||||||
HeroLines,
|
HeroLines,
|
||||||
ParticleNetwork,
|
ParticleNetwork,
|
||||||
@@ -21,26 +22,23 @@ import {
|
|||||||
Label,
|
Label,
|
||||||
MonoLabel,
|
MonoLabel,
|
||||||
} from "../../src/components/Typography";
|
} from "../../src/components/Typography";
|
||||||
import { BackgroundGrid, Card, Container } from "../../src/components/Layout";
|
import { Card, Container } from "../../src/components/Layout";
|
||||||
import { Button } from "../../src/components/Button";
|
import { Button } from "../../src/components/Button";
|
||||||
import { IconList, IconListItem } from "../../src/components/IconList";
|
import { IconList, IconListItem } from "../../src/components/IconList";
|
||||||
|
import {
|
||||||
|
GradientMesh,
|
||||||
|
CodeSnippet,
|
||||||
|
AbstractCircuit,
|
||||||
|
} from "../../src/components/Effects";
|
||||||
|
import { Marker } from "../../src/components/Marker";
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
{/* Background Elements */}
|
<AbstractCircuit />
|
||||||
<ParticleNetwork className="opacity-20" />
|
|
||||||
<BackgroundGrid />
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative pt-32 pb-24 overflow-hidden border-b border-slate-50">
|
<section className="relative pt-32 pb-24 overflow-hidden border-b border-slate-50">
|
||||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full opacity-10 pointer-events-none">
|
|
||||||
<HeroLines className="w-full h-full" />
|
|
||||||
</div>
|
|
||||||
<div className="absolute right-0 top-0 w-96 h-96 opacity-5 pointer-events-none">
|
|
||||||
<GridLines />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Container variant="narrow" className="relative z-10">
|
<Container variant="narrow" className="relative z-10">
|
||||||
<div className="flex flex-col items-center text-center space-y-12">
|
<div className="flex flex-col items-center text-center space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
@@ -75,10 +73,11 @@ export default function AboutPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Über <span className="text-slate-200">mich.</span>
|
Über <span className="text-slate-400">mich.</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description="Warum ich tue, was ich tue – und wie Sie davon profitieren."
|
description="15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen halten."
|
||||||
|
backLink={{ href: "/", label: "Zurück" }}
|
||||||
className="pt-0 md:pt-0"
|
className="pt-0 md:pt-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +88,7 @@ export default function AboutPage() {
|
|||||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-16 bg-gradient-to-b from-transparent to-slate-200" />
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-16 bg-gradient-to-b from-transparent to-slate-200" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 01: Experience */}
|
{/* Section 01: Story */}
|
||||||
<Section
|
<Section
|
||||||
number="01"
|
number="01"
|
||||||
title="Erfahrung"
|
title="Erfahrung"
|
||||||
@@ -99,10 +98,8 @@ export default function AboutPage() {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
15 Jahre Web-Entwicklung. <br />
|
Vom Designer <br />
|
||||||
<span className="text-slate-200">
|
<span className="text-slate-400">zum Architekten.</span>
|
||||||
Vom Designer zum Architekten.
|
|
||||||
</span>
|
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
@@ -110,17 +107,20 @@ export default function AboutPage() {
|
|||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||||
Ich habe Agenturen, Konzerne und Startups von innen gesehen.
|
Agenturen, Konzerne, Startups – ich habe die Branche von allen
|
||||||
Dabei habe ich gelernt, what really counts:{" "}
|
Seiten kennengelernt. Was hängen geblieben ist:{" "}
|
||||||
<span className="text-slate-900">
|
<span className="text-slate-900">
|
||||||
Ergebnisse, nicht Prozesse.
|
<Marker delay={0.2} color="rgba(148,163,184,0.15)">
|
||||||
|
Ergebnisse zählen.
|
||||||
|
</Marker>{" "}
|
||||||
|
Nicht der Weg dorthin.
|
||||||
</span>
|
</span>
|
||||||
</LeadText>
|
</LeadText>
|
||||||
<IconList className="space-y-4">
|
<IconList className="space-y-4">
|
||||||
{[
|
{[
|
||||||
"Komplexe Systeme vereinfacht",
|
"Frontend, Backend, Infrastruktur – Fullstack",
|
||||||
"Performance-Probleme gelöst",
|
"Komplexe Systeme auf das Wesentliche reduziert",
|
||||||
"Nachhaltige Software-Architekturen gebaut",
|
"Performance-Probleme systematisch gelöst",
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<IconListItem key={i} bullet>
|
<IconListItem key={i} bullet>
|
||||||
<BodyText className="text-lg">{item}</BodyText>
|
<BodyText className="text-lg">{item}</BodyText>
|
||||||
@@ -137,8 +137,7 @@ export default function AboutPage() {
|
|||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
<H4 className="text-2xl mb-6">
|
<H4 className="text-2xl mb-6">
|
||||||
Mein Fokus heute: Direkte Zusammenarbeit ohne
|
Heute: Direkte Zusammenarbeit ohne Reibungsverluste.
|
||||||
Reibungsverluste.
|
|
||||||
</H4>
|
</H4>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{["Effizient", "Pragmatisch", "Verlässlich"].map((tag, i) => (
|
{["Effizient", "Pragmatisch", "Verlässlich"].map((tag, i) => (
|
||||||
@@ -156,64 +155,77 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 02: Responsibility */}
|
{/* Section 02: Arbeitsweise – HOW I work */}
|
||||||
<Section
|
<Section
|
||||||
number="02"
|
number="02"
|
||||||
title="Verantwortung"
|
title="Arbeitsweise"
|
||||||
variant="gray"
|
variant="gray"
|
||||||
borderTop
|
borderTop
|
||||||
illustration={<ResponsibilityIllustration className="w-24 h-24" />}
|
illustration={<ResponsibilityIllustration className="w-24 h-24" />}
|
||||||
|
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Ich stehe für meine <br />
|
So läuft ein Projekt <br />
|
||||||
<span className="text-slate-200">Arbeit gerade.</span>
|
<span className="text-slate-400">bei mir ab.</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
{/* Timeline Steps */}
|
||||||
<div className="md:col-span-8 space-y-8">
|
<div className="space-y-1 relative">
|
||||||
<Reveal delay={0.1}>
|
{/* Connecting line */}
|
||||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
<div className="absolute left-[15px] top-8 bottom-8 w-px bg-slate-200 hidden md:block" />
|
||||||
In der klassischen Agenturwelt verschwindet Verantwortung oft
|
|
||||||
hinter Hierarchien. Bei mir gibt es nur{" "}
|
{[
|
||||||
<span className="text-slate-900">einen Ansprechpartner:</span>{" "}
|
{
|
||||||
Mich.
|
step: "01",
|
||||||
</LeadText>
|
title: "Briefing",
|
||||||
</Reveal>
|
desc: "Sie beschreiben Ihr Vorhaben. Ich höre zu und stelle die richtigen Fragen.",
|
||||||
<Reveal delay={0.2}>
|
},
|
||||||
<Card
|
{
|
||||||
variant="white"
|
step: "02",
|
||||||
padding="normal"
|
title: "Angebot",
|
||||||
className="flex flex-row items-start gap-6 group"
|
desc: "Ein Fixpreis-Angebot mit klarem Leistungsumfang. Keine Überraschungen.",
|
||||||
>
|
},
|
||||||
<div className="w-12 h-12 bg-slate-900 text-white rounded-xl flex items-center justify-center shrink-0 font-bold text-xl group-hover:rotate-12 transition-transform duration-500">
|
{
|
||||||
!
|
step: "03",
|
||||||
|
title: "Umsetzung",
|
||||||
|
desc: "Schnelle Iterationen. Sie sehen regelmäßig den Fortschritt und geben Feedback.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: "04",
|
||||||
|
title: "Launch",
|
||||||
|
desc: "Go-Live mit automatisiertem Deployment. Dokumentiert und übergabereif.",
|
||||||
|
},
|
||||||
|
].map((item, i) => (
|
||||||
|
<Reveal key={i} delay={0.1 + i * 0.1}>
|
||||||
|
<div className="flex gap-6 py-6 group">
|
||||||
|
<div className="relative z-10 shrink-0">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white border border-slate-200 flex items-center justify-center group-hover:border-slate-400 group-hover:shadow-md transition-all duration-500">
|
||||||
|
<span className="text-[9px] font-mono font-bold text-slate-400 group-hover:text-slate-900 transition-colors">
|
||||||
|
{item.step}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BodyText className="text-slate-900 font-medium text-lg md:text-xl leading-relaxed">
|
<div className="space-y-2 pt-1">
|
||||||
Ich übernehme die volle Verantwortung für die technische
|
<H4 className="text-xl">{item.title}</H4>
|
||||||
Umsetzung und Qualität Ihres Projekts. Ohne Ausreden.
|
<BodyText className="text-slate-500">{item.desc}</BodyText>
|
||||||
</BodyText>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 03: Systems */}
|
{/* Section 03: Philosophie – what drives me */}
|
||||||
<Section
|
<Section number="03" title="Philosophie" borderTop>
|
||||||
number="03"
|
|
||||||
title="Philosophie"
|
|
||||||
borderTop
|
|
||||||
illustration={<ConceptSystem className="w-24 h-24" />}
|
|
||||||
>
|
|
||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Nachhaltigkeit durch <br />
|
Ich stehe für <br />
|
||||||
<span className="text-slate-200">sauberen Code.</span>
|
<span className="text-slate-400">meine Arbeit gerade.</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
@@ -221,24 +233,24 @@ export default function AboutPage() {
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<Reveal delay={0.1}>
|
<Reveal delay={0.1}>
|
||||||
<LeadText className="text-xl text-slate-400">
|
<LeadText className="text-xl text-slate-400">
|
||||||
Ich baue keine Wegwerf-Produkte. Meine Systeme sind so
|
Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt,
|
||||||
konzipiert, dass sie mit Ihrem Unternehmen{" "}
|
liegt die Verantwortung bei mir – und ich{" "}
|
||||||
<span className="text-slate-900">wachsen können.</span>
|
<span className="text-slate-900">
|
||||||
|
<Marker color="rgba(255,235,59,0.5)">löse es.</Marker>
|
||||||
|
</span>
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{[
|
{[
|
||||||
"Skalierbar",
|
"Vollständige Transparenz",
|
||||||
"Wartbar",
|
"Ein Ansprechpartner",
|
||||||
"Performant",
|
"Messbare Qualität",
|
||||||
"Sicher",
|
"Langfristige Partnerschaft",
|
||||||
"Unabhängig",
|
|
||||||
"Zukunftssicher",
|
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<Reveal key={i} delay={0.2 + i * 0.05}>
|
<Reveal key={i} delay={0.2 + i * 0.05}>
|
||||||
<div className="flex items-center gap-3 group">
|
<div className="flex items-center gap-3 group">
|
||||||
<div className="w-5 h-5 rounded-full bg-slate-50 flex items-center justify-center group-hover:bg-slate-900 transition-colors duration-500">
|
<div className="w-6 h-6 rounded-full bg-white border border-slate-200 flex items-center justify-center shrink-0 group-hover:bg-slate-900 group-hover:border-slate-900 group-hover:shadow-lg group-hover:shadow-blue-500/10 transition-all duration-300">
|
||||||
<Check className="w-2.5 h-2.5 text-slate-400 group-hover:text-white" />
|
<Check className="w-3 h-3 text-slate-400 group-hover:text-white transition-colors duration-300" />
|
||||||
</div>
|
</div>
|
||||||
<Label className="text-slate-900">{item}</Label>
|
<Label className="text-slate-900">{item}</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,126 +258,47 @@ export default function AboutPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Decorative terminal */}
|
||||||
<Reveal delay={0.3}>
|
<Reveal delay={0.3}>
|
||||||
<Card
|
<CodeSnippet variant="terminal" className="opacity-70" />
|
||||||
variant="dark"
|
|
||||||
padding="normal"
|
|
||||||
className="relative rounded-2xl overflow-hidden group"
|
|
||||||
>
|
|
||||||
<div className="absolute top-0 right-0 w-48 h-48 bg-white/5 -translate-y-24 translate-x-24 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-1000" />
|
|
||||||
<H4 className="text-white text-2xl mb-6 relative z-10">
|
|
||||||
Kein Vendor Lock-in.
|
|
||||||
</H4>
|
|
||||||
<LeadText className="text-slate-400 text-lg relative z-10 leading-relaxed">
|
|
||||||
Sie behalten die volle Kontrolle über Ihren Code und Ihre
|
|
||||||
Daten. Keine Abhängigkeit von proprietären Systemen.
|
|
||||||
</LeadText>
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 04: Result */}
|
{/* Section 04: CTA */}
|
||||||
<Section
|
<Section
|
||||||
number="04"
|
number="04"
|
||||||
title="Ergebnis"
|
|
||||||
borderTop
|
|
||||||
illustration={<ResultIllustration className="w-24 h-24" />}
|
|
||||||
>
|
|
||||||
<div className="space-y-16">
|
|
||||||
<Reveal>
|
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
|
||||||
Was Sie von mir <br />
|
|
||||||
<span className="text-slate-200">erwarten können.</span>
|
|
||||||
</H3>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Label>Kein:</Label>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{[
|
|
||||||
"Agentur-Zirkus",
|
|
||||||
"Meeting-Marathon",
|
|
||||||
"Ticket-Wahnsinn",
|
|
||||||
"CMS-Frust",
|
|
||||||
].map((item, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="px-4 py-2 border border-slate-100 rounded-full bg-slate-50/50"
|
|
||||||
>
|
|
||||||
<BodyText className="text-slate-400 line-through text-base mb-0">
|
|
||||||
{item}
|
|
||||||
</BodyText>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Label className="text-slate-900">Sondern (fix):</Label>
|
|
||||||
<IconList className="space-y-8">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
label: "Direkte Kommunikation",
|
|
||||||
desc: "Kurze Wege, schnelle Entscheidungen.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Echte Expertise",
|
|
||||||
desc: "Fundiertes Wissen aus 15 Jahren Praxis.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Messbare Qualität",
|
|
||||||
desc: "Code, der hält, was er verspricht.",
|
|
||||||
},
|
|
||||||
].map((item, i) => (
|
|
||||||
<IconListItem key={i} check>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<H4 className="text-xl leading-none">{item.label}</H4>
|
|
||||||
<BodyText className="text-base text-slate-400">
|
|
||||||
{item.desc}
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</IconListItem>
|
|
||||||
))}
|
|
||||||
</IconList>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* Section 05: Today */}
|
|
||||||
<Section
|
|
||||||
number="05"
|
|
||||||
title="Kontakt"
|
title="Kontakt"
|
||||||
variant="gray"
|
variant="gray"
|
||||||
borderTop
|
borderTop
|
||||||
illustration={<ContactIllustration className="w-24 h-24" />}
|
illustration={<ContactIllustration className="w-24 h-24" />}
|
||||||
|
effects={<GradientMesh variant="metallic" className="opacity-60" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Bereit für eine <br />
|
Bereit für eine <br />
|
||||||
<span className="text-slate-200">Zusammenarbeit?</span>
|
<span className="text-slate-400">Zusammenarbeit?</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
variant="white"
|
variant="glass"
|
||||||
hover={false}
|
hover={false}
|
||||||
padding="large"
|
padding="large"
|
||||||
|
techBorder
|
||||||
className="rounded-3xl shadow-xl relative overflow-hidden group"
|
className="rounded-3xl shadow-xl relative overflow-hidden group"
|
||||||
>
|
>
|
||||||
<div className="absolute top-0 right-0 w-96 h-96 bg-slate-50 -translate-y-1/2 translate-x-1/2 rounded-full blur-[80px] group-hover:bg-slate-100 transition-colors duration-1000" />
|
|
||||||
|
|
||||||
<div className="relative z-10 space-y-8">
|
<div className="relative z-10 space-y-8">
|
||||||
<LeadText className="text-2xl md:text-4xl leading-tight max-w-2xl text-slate-400">
|
<LeadText className="text-2xl md:text-4xl leading-tight max-w-2xl text-slate-400">
|
||||||
Lassen Sie uns gemeinsam etwas bauen, das{" "}
|
Lassen Sie uns gemeinsam etwas bauen, das{" "}
|
||||||
<span className="text-slate-900">wirklich funktioniert.</span>
|
<span className="text-slate-900">
|
||||||
|
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||||
|
wirklich funktioniert.
|
||||||
|
</Marker>
|
||||||
|
</span>
|
||||||
</LeadText>
|
</LeadText>
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
|
|||||||
@@ -1,77 +1,113 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { MediumCard } from '../../src/components/MediumCard';
|
import { MediumCard } from "../../src/components/MediumCard";
|
||||||
import { SearchBar } from '../../src/components/SearchBar';
|
import { SearchBar } from "../../src/components/SearchBar";
|
||||||
import { Tag } from '../../src/components/Tag';
|
import { Tag } from "../../src/components/Tag";
|
||||||
import { blogPosts } from '../../src/data/blogPosts';
|
import { blogPosts } from "../../src/data/blogPosts";
|
||||||
import { PageHeader } from '../../src/components/PageHeader';
|
import { PageHeader } from "../../src/components/PageHeader";
|
||||||
import { Reveal } from '../../src/components/Reveal';
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
|
import { Section } from "../../src/components/Section";
|
||||||
|
import { AbstractCircuit, GradientMesh } from "../../src/components/Effects";
|
||||||
|
import { Label } from "../../src/components/Typography";
|
||||||
|
|
||||||
export default function BlogPage() {
|
export default function BlogPage() {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
|
|
||||||
|
|
||||||
// Sort posts by date
|
// Memoize allPosts to prevent infinite re-render loop
|
||||||
const allPosts = [...blogPosts].sort((a, b) =>
|
const allPosts = React.useMemo(
|
||||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
() =>
|
||||||
|
[...blogPosts].sort(
|
||||||
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||||
|
),
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get unique tags
|
const [filteredPosts, setFilteredPosts] = useState(allPosts);
|
||||||
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
|
|
||||||
|
// Memoize allTags
|
||||||
|
const allTags = React.useMemo(
|
||||||
|
() => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))),
|
||||||
|
[allPosts],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = searchQuery.toLowerCase().trim();
|
const query = searchQuery.toLowerCase().trim();
|
||||||
if (query.startsWith('#')) {
|
if (query.startsWith("#")) {
|
||||||
const tag = query.slice(1);
|
const tag = query.slice(1);
|
||||||
setFilteredPosts(allPosts.filter(post =>
|
setFilteredPosts(
|
||||||
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
|
allPosts.filter((post) =>
|
||||||
));
|
post.tags?.some((t) => t.toLowerCase() === tag.toLowerCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setFilteredPosts(allPosts.filter(post => {
|
setFilteredPosts(
|
||||||
const title = post.title.toLowerCase();
|
allPosts.filter((post) => {
|
||||||
const description = post.description.toLowerCase();
|
const title = post.title.toLowerCase();
|
||||||
const tags = (post.tags || []).join(' ').toLowerCase();
|
const description = post.description.toLowerCase();
|
||||||
return title.includes(query) || description.includes(query) || tags.includes(query);
|
const tags = (post.tags || []).join(" ").toLowerCase();
|
||||||
}));
|
return (
|
||||||
|
title.includes(query) ||
|
||||||
|
description.includes(query) ||
|
||||||
|
tags.includes(query)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [searchQuery]);
|
}, [searchQuery, allPosts]);
|
||||||
|
|
||||||
const filterByTag = (tag: string) => {
|
const filterByTag = (tag: string) => {
|
||||||
setSearchQuery(`#${tag}`);
|
setSearchQuery(`#${tag}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
|
<div className="flex flex-col bg-white overflow-hidden relative min-h-screen">
|
||||||
<PageHeader
|
<AbstractCircuit />
|
||||||
title={<>Blog <br /><span className="text-slate-200">& Notes.</span></>}
|
|
||||||
description="A public notebook of things I figured out, mistakes I made, and tools I tested."
|
<PageHeader
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Blog <br />
|
||||||
|
<span className="text-slate-400">& Notes.</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description="Ein technisches Notizbuch über Lösungen, Fehler und Werkzeuge."
|
||||||
|
backLink={{ href: "/", label: "Zurück" }}
|
||||||
backgroundSymbol="B"
|
backgroundSymbol="B"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="narrow-container">
|
<Section
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-16">
|
number="01"
|
||||||
|
title="Journal"
|
||||||
|
borderTop
|
||||||
|
effects={<GradientMesh variant="metallic" className="opacity-50" />}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||||
{/* Sidebar / Filter area */}
|
{/* Sidebar / Filter area */}
|
||||||
<div className="md:col-span-4">
|
<div className="lg:col-span-4 lg:order-2">
|
||||||
<div className="sticky top-32 space-y-16">
|
<div className="sticky top-32 space-y-16">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Suchen</h3>
|
<Label className="text-slate-400 uppercase tracking-[0.3em]">
|
||||||
|
Suchen
|
||||||
|
</Label>
|
||||||
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
{allTags.length > 0 && (
|
{allTags.length > 0 && (
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Themen</h3>
|
<Label className="text-slate-400 uppercase tracking-[0.3em]">
|
||||||
|
Themen
|
||||||
|
</Label>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{allTags.map((tag, index) => (
|
{allTags.map((tag, index) => (
|
||||||
<button
|
<button
|
||||||
key={tag}
|
key={tag}
|
||||||
onClick={() => filterByTag(tag)}
|
onClick={() => filterByTag(tag)}
|
||||||
className="text-left"
|
className="text-left group"
|
||||||
>
|
>
|
||||||
<Tag tag={tag} index={index} />
|
<Tag tag={tag} index={index} />
|
||||||
</button>
|
</button>
|
||||||
@@ -84,11 +120,13 @@ export default function BlogPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Posts area */}
|
{/* Posts area */}
|
||||||
<div className="md:col-span-8">
|
<div className="lg:col-span-8 lg:order-1">
|
||||||
<div id="posts-container" className="flex flex-col gap-8">
|
<div id="posts-container" className="flex flex-col gap-12">
|
||||||
{filteredPosts.length === 0 ? (
|
{filteredPosts.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="py-24 text-center border border-dashed border-slate-200 rounded-3xl">
|
||||||
<p>No posts found matching your criteria.</p>
|
<p className="text-slate-400 font-mono text-sm uppercase tracking-widest">
|
||||||
|
Keine Beiträge gefunden.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredPosts.map((post, i) => (
|
filteredPosts.map((post, i) => (
|
||||||
@@ -100,7 +138,7 @@ export default function BlogPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import {
|
|||||||
BodyText,
|
BodyText,
|
||||||
} from "../../../src/components/Typography";
|
} from "../../../src/components/Typography";
|
||||||
import { BackgroundGrid, Container } from "../../../src/components/Layout";
|
import { BackgroundGrid, Container } from "../../../src/components/Layout";
|
||||||
import { MotionButton } from "../../../src/components/Button";
|
import Link from "next/link";
|
||||||
|
import { Button } from "../../../src/components/Button";
|
||||||
import { IframeSection } from "../../../src/components/IframeSection";
|
import { IframeSection } from "../../../src/components/IframeSection";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
ArrowLeft,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Cpu,
|
Cpu,
|
||||||
Server,
|
Server,
|
||||||
@@ -26,6 +28,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Marker } from "../../../src/components/Marker";
|
import { Marker } from "../../../src/components/Marker";
|
||||||
|
import { GlitchText } from "../../../src/components/GlitchText";
|
||||||
|
|
||||||
export default function KLZCablesCaseStudy() {
|
export default function KLZCablesCaseStudy() {
|
||||||
const { scrollYProgress } = useScroll();
|
const { scrollYProgress } = useScroll();
|
||||||
@@ -50,6 +53,15 @@ export default function KLZCablesCaseStudy() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Container variant="narrow" className="relative z-10">
|
<Container variant="narrow" className="relative z-10">
|
||||||
|
<Reveal>
|
||||||
|
<Link
|
||||||
|
href="/case-studies"
|
||||||
|
className="inline-flex items-center gap-2 text-slate-400 hover:text-slate-900 mb-12 transition-colors font-bold text-[10px] uppercase tracking-[0.4em] group"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-3 h-3 group-hover:-translate-x-1 transition-transform" />{" "}
|
||||||
|
Zurück
|
||||||
|
</Link>
|
||||||
|
</Reveal>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal direction="down" blur>
|
<Reveal direction="down" blur>
|
||||||
<div className="inline-flex items-center gap-6">
|
<div className="inline-flex items-center gap-6">
|
||||||
@@ -71,13 +83,14 @@ export default function KLZCablesCaseStudy() {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal delay={0.1} direction="up" scale={0.98} blur>
|
<GlitchText
|
||||||
<H1 className="text-6xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900">
|
as="h1"
|
||||||
KLZ Cables
|
className="text-6xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900"
|
||||||
<br />
|
>
|
||||||
<span className="text-slate-100">Case Study.</span>
|
KLZ Cables
|
||||||
</H1>
|
</GlitchText>
|
||||||
</Reveal>
|
<br />
|
||||||
|
<span className="text-slate-100">Case Study.</span>
|
||||||
|
|
||||||
<Reveal delay={0.2} direction="right" blur>
|
<Reveal delay={0.2} direction="right" blur>
|
||||||
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-8 md:pl-12">
|
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-8 md:pl-12">
|
||||||
@@ -126,14 +139,19 @@ export default function KLZCablesCaseStudy() {
|
|||||||
borderBottom
|
borderBottom
|
||||||
containerVariant="normal"
|
containerVariant="normal"
|
||||||
>
|
>
|
||||||
|
{/* Binary overlay background */}
|
||||||
|
<div className="absolute top-0 right-0 p-8 opacity-[0.03] select-none pointer-events-none font-mono text-[10px] hidden md:block">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i}>01001101 01001001 01001110 01010100</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 lg:gap-24 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 lg:gap-24 items-start">
|
||||||
<div className="md:col-span-12 mb-12">
|
<div className="md:col-span-12 mb-12">
|
||||||
<Reveal direction="left" blur>
|
<H2 className="text-5xl md:text-8xl tracking-tighter mb-12">
|
||||||
<H2 className="text-5xl md:text-8xl tracking-tighter mb-12">
|
<GlitchText>Architektur-</GlitchText> <br />
|
||||||
Architektur- <br />
|
Refactor.
|
||||||
Refactor.
|
</H2>
|
||||||
</H2>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-7 space-y-12">
|
<div className="md:col-span-7 space-y-12">
|
||||||
<Reveal delay={0.1} direction="up" blur>
|
<Reveal delay={0.1} direction="up" blur>
|
||||||
@@ -217,9 +235,16 @@ export default function KLZCablesCaseStudy() {
|
|||||||
Infrastructure Validation
|
Infrastructure Validation
|
||||||
</Label>
|
</Label>
|
||||||
<H3 className="text-5xl md:text-8xl tracking-tighter">
|
<H3 className="text-5xl md:text-8xl tracking-tighter">
|
||||||
Global Hub.
|
<GlitchText>Global Hub.</GlitchText>
|
||||||
</H3>
|
</H3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Binary overlay left */}
|
||||||
|
<div className="absolute left-0 bottom-0 p-8 opacity-[0.03] select-none pointer-events-none font-mono text-[10px] hidden md:block group-hover:opacity-10 transition-opacity duration-1000">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i}>HANDSHAKE_0x00{i}A // SYNC_ACTIVE</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||||
@@ -298,12 +323,10 @@ export default function KLZCablesCaseStudy() {
|
|||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||||
<div className="lg:col-span-12 mb-12 text-center lg:text-left relative z-10">
|
<div className="lg:col-span-12 mb-12 text-center lg:text-left relative z-10">
|
||||||
<Reveal direction="down" blur>
|
<H3 className="text-4xl md:text-6xl max-w-4xl tracking-tighter">
|
||||||
<H3 className="text-4xl md:text-6xl max-w-4xl tracking-tighter">
|
Fokus auf <br />
|
||||||
Fokus auf <br />
|
<Marker delay={0.2}>Spezifikationen.</Marker>
|
||||||
<Marker delay={0.2}>Spezifikationen.</Marker>
|
</H3>
|
||||||
</H3>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-8 relative group">
|
<div className="lg:col-span-8 relative group">
|
||||||
@@ -443,43 +466,80 @@ export default function KLZCablesCaseStudy() {
|
|||||||
className="!pb-32"
|
className="!pb-32"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 items-center">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 items-center">
|
||||||
|
<div className="lg:col-span-5 space-y-12">
|
||||||
|
<Reveal direction="left" blur>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Label className="text-slate-400">Interaction Layer</Label>
|
||||||
|
<H3 className="text-5xl md:text-7xl tracking-tighter text-slate-900">
|
||||||
|
Direkter Draht.
|
||||||
|
</H3>
|
||||||
|
<BodyText className="text-xl text-slate-500 font-serif italic leading-relaxed">
|
||||||
|
Das Kontakt-System wurde auf maximale Reduktion getrimmt. Ein
|
||||||
|
deterministischer Kanal zwischen technischem Bedarf und
|
||||||
|
individueller Beratung – ohne Umwege, ohne Rauschen.
|
||||||
|
</BodyText>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.2} direction="left" blur>
|
||||||
|
<div className="grid grid-cols-2 gap-8 border-t border-slate-100 pt-10">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<MonoLabel className="text-slate-400 text-[9px]">
|
||||||
|
RESPONSE_TIME
|
||||||
|
</MonoLabel>
|
||||||
|
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||||
|
< 120ms
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<MonoLabel className="text-slate-400 text-[9px]">
|
||||||
|
PROTOCOL
|
||||||
|
</MonoLabel>
|
||||||
|
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||||
|
mTLS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<MonoLabel className="text-slate-400 text-[9px]">
|
||||||
|
AVAILABILITY
|
||||||
|
</MonoLabel>
|
||||||
|
<div className="text-xl font-bold text-[rgba(129,199,132,1)] font-mono">
|
||||||
|
99.9%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<MonoLabel className="text-slate-400 text-[9px]">
|
||||||
|
ENCRYPTION
|
||||||
|
</MonoLabel>
|
||||||
|
<div className="text-xl font-bold text-slate-900 font-mono">
|
||||||
|
AES-256
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-7">
|
<div className="lg:col-span-7">
|
||||||
<Reveal direction="left" scale={0.98} blur>
|
<Reveal direction="right" scale={0.98} blur>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.01 }}
|
whileHover={{ scale: 1.002 }}
|
||||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
transition={{ type: "spring", stiffness: 100, damping: 30 }}
|
||||||
className="relative h-[800px] w-full"
|
className="relative rounded-[2.5rem] overflow-hidden shadow-2xl shadow-slate-200/50 ring-1 ring-slate-100"
|
||||||
>
|
>
|
||||||
<IframeSection
|
<IframeSection
|
||||||
src="/showcase/klz-cables.com/contact.html"
|
src="/showcase/klz-cables.com/contact.html"
|
||||||
height="100%"
|
height="750px"
|
||||||
desktopWidth={1200}
|
desktopWidth={1200}
|
||||||
|
mobileWidth={390}
|
||||||
allowScroll
|
allowScroll
|
||||||
browserFrame
|
browserFrame
|
||||||
className="h-full w-full no-scrollbar"
|
className="w-full no-scrollbar"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-5 space-y-10">
|
|
||||||
<Reveal direction="right" blur>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Label className="text-slate-400">Conversion Layer</Label>
|
|
||||||
<H3 className="text-4xl md:text-6xl tracking-tighter">
|
|
||||||
Direkter Draht.
|
|
||||||
</H3>
|
|
||||||
<BodyText className="text-xl text-slate-500 font-serif italic">
|
|
||||||
Das Kontakt-System wurde auf maximale Reduktion getrimmt.
|
|
||||||
Keine unnötigen Hürden, sondern ein direkter
|
|
||||||
Kommunikations-Kanal zwischen technischem Bedarf und
|
|
||||||
individueller Beratung.
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
|
{/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
|
||||||
<section className="py-40 md:py-64 bg-white relative overflow-hidden border-t border-slate-100">
|
<section className="py-40 md:py-64 bg-white relative overflow-hidden border-t border-slate-100">
|
||||||
<BackgroundGrid />
|
<BackgroundGrid />
|
||||||
@@ -540,15 +600,15 @@ export default function KLZCablesCaseStudy() {
|
|||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.5} direction="up" blur className="pt-6">
|
<Reveal delay={0.5} direction="up" blur className="pt-6">
|
||||||
<MotionButton
|
<Button
|
||||||
href="/contact"
|
href="/contact"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
className="w-full py-8 text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700"
|
className="w-full py-8 text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700"
|
||||||
>
|
>
|
||||||
Architektur-Audit anfragen
|
Jetzt anfragen
|
||||||
<ArrowRight className="inline-block ml-4 w-6 h-6 group-hover:translate-x-4 transition-transform duration-700" />
|
<ArrowRight className="inline-block ml-4 w-6 h-6 group-hover:translate-x-4 transition-transform duration-700" />
|
||||||
</MotionButton>
|
</Button>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,104 +1,175 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
import { PageHeader } from "../../src/components/PageHeader";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
import { H3, LeadText, Label } from "../../src/components/Typography";
|
import { H3, LeadText, BodyText, Label } from "../../src/components/Typography";
|
||||||
import { BackgroundGrid, Card } from "../../src/components/Layout";
|
import { Card } from "../../src/components/Layout";
|
||||||
import { MotionButton } from "../../src/components/Button";
|
import { Button } from "../../src/components/Button";
|
||||||
import Image from "next/image";
|
import { GradientMesh, AbstractCircuit } from "../../src/components/Effects";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default function CaseStudiesPage() {
|
export default function CaseStudiesPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white overflow-hidden relative min-h-screen">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<BackgroundGrid />
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Case Studies: <br />
|
Case <span className="text-slate-400">Studies.</span>
|
||||||
<span className="text-slate-200">Qualität in jedem Detail.</span>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description="Ein Blick hinter die Kulissen ausgewählter Projekte. Von der ersten Idee bis zum fertigen Hochleistungssystem."
|
description="Ergebnisse statt Versprechen. Was ich gebaut habe und was es bewirkt."
|
||||||
backLink={{ href: "/", label: "Zurück" }}
|
backLink={{ href: "/", label: "Zurück" }}
|
||||||
backgroundSymbol="C"
|
backgroundSymbol="C"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section number="01" title="Projekte" borderTop>
|
{/* Featured Case Study */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
<Section
|
||||||
<Reveal>
|
number="01"
|
||||||
<Card variant="white" className="group overflow-hidden">
|
title="Showcase"
|
||||||
<div className="aspect-video relative overflow-hidden rounded-xl mb-8 bg-slate-100 border border-slate-100">
|
borderTop
|
||||||
{/* We'll use a placeholder or a screenshot if available.
|
effects={<GradientMesh variant="metallic" className="opacity-70" />}
|
||||||
Since we have the cloned site, we could technically iframe a preview here too,
|
>
|
||||||
but a static image or a styled div is more standard for a card. */}
|
<Reveal>
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-[#0117bf] transition-transform duration-700 group-hover:scale-105 p-12">
|
<a href="/case-studies/klz-cables" className="block group">
|
||||||
<Image
|
<Card
|
||||||
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
variant="glass"
|
||||||
alt="KLZ Cables Logo"
|
padding="none"
|
||||||
width={200}
|
techBorder
|
||||||
height={200}
|
className="overflow-hidden relative group min-h-[500px] flex flex-col md:flex-row"
|
||||||
className="w-full h-auto max-w-[240px]"
|
>
|
||||||
/>
|
{/* Brand Gradient Background */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(14,165,233,0.08)_0%,transparent_50%),radial-gradient(circle_at_70%_70%,rgba(99,102,241,0.05)_0%,transparent_50%)]" />
|
||||||
|
|
||||||
|
{/* Left Column: Content */}
|
||||||
|
<div className="flex-1 p-8 md:p-12 relative z-10 flex flex-col justify-between">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
||||||
|
alt="KLZ Logo"
|
||||||
|
className="h-8 invert opacity-80 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
|
/>
|
||||||
|
<div className="h-px w-12 bg-slate-100" />
|
||||||
|
<Label className="text-slate-400">Case Study 2025</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<H3 className="text-4xl md:text-6xl tracking-tighter">
|
||||||
|
KLZ <span className="text-slate-300">Cables</span>
|
||||||
|
</H3>
|
||||||
|
<LeadText className="text-slate-500 text-lg md:text-xl max-w-xl leading-relaxed">
|
||||||
|
Engineering eines industriellen B2B-Systems mit
|
||||||
|
<span className="text-slate-900 font-medium">
|
||||||
|
{" "}
|
||||||
|
automatisierter Asset-Pipeline
|
||||||
|
</span>{" "}
|
||||||
|
und hochperformantem Headless-Stack.
|
||||||
|
</LeadText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
|
{["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map(
|
||||||
|
(tag, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="px-2.5 py-1 border border-slate-100 bg-white/50 rounded-md text-[9px] font-mono text-slate-400 uppercase tracking-widest group-hover:border-slate-300 transition-colors duration-500"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-12">
|
||||||
|
<div className="inline-flex items-center gap-3 text-sm font-bold text-slate-400 group-hover:text-slate-900 transition-all duration-500">
|
||||||
|
<span>EXPLORE PROJECT</span>
|
||||||
|
<ArrowRight className="w-4 h-4 group-hover:translate-x-2 transition-transform duration-500" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* Right Column: Visual/Technical Decor */}
|
||||||
<Label>Infrastructure & Energy</Label>
|
<div className="w-full md:w-1/3 bg-slate-50 relative overflow-hidden border-t md:border-t-0 md:border-l border-slate-100">
|
||||||
<H3 className="group-hover:text-slate-900 transition-colors">
|
<div className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[8px] p-4 flex flex-col gap-1 overflow-hidden">
|
||||||
KLZ Cables – Digitaler Netzbau
|
{Array.from({ length: 40 }).map((_, i) => (
|
||||||
</H3>
|
<div key={i} className="whitespace-nowrap">
|
||||||
<LeadText className="text-base line-clamp-3">
|
{Array.from({ length: 10 })
|
||||||
Wie wir eine komplexe WordPress-Struktur in ein performantes,
|
.map((_, j) => (
|
||||||
sauberes und langlebiges Web-System verwandelt haben. Fokus
|
<span
|
||||||
auf Performance, SEO und Benutzerführung.
|
key={j}
|
||||||
</LeadText>
|
className={
|
||||||
|
Math.random() > 0.5
|
||||||
|
? "text-slate-900"
|
||||||
|
: "text-slate-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Math.floor(Math.random() * 2)}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
.join(" ")}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-4">
|
{/* Abstract "Cable" lines */}
|
||||||
<MotionButton href="/case-studies/klz-cables">
|
<div className="absolute inset-0 flex items-center justify-center p-12">
|
||||||
Case Study lesen
|
<div className="w-full h-full relative">
|
||||||
</MotionButton>
|
{[1, 2, 3].map((v) => (
|
||||||
|
<motion.div
|
||||||
|
key={v}
|
||||||
|
initial={{ scaleY: 0 }}
|
||||||
|
whileInView={{ scaleY: 1 }}
|
||||||
|
transition={{ duration: 2, delay: 0.5 + v * 0.2 }}
|
||||||
|
className="absolute inset-y-0 border-r border-slate-200 origin-top"
|
||||||
|
style={{ right: `${v * 25}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-8 right-8 text-[10px] font-mono text-slate-300 rotate-90 origin-right uppercase tracking-[0.3em]">
|
||||||
|
Industrial Grade
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Reveal>
|
</a>
|
||||||
|
</Reveal>
|
||||||
<Reveal delay={0.2}>
|
|
||||||
<div className="h-full flex flex-col justify-center border-2 border-dashed border-slate-100 rounded-3xl p-12 text-center space-y-4">
|
|
||||||
<Label>Demnächst</Label>
|
|
||||||
<H3 className="text-slate-200">
|
|
||||||
Weitere Projekte sind in Arbeit.
|
|
||||||
</H3>
|
|
||||||
<LeadText className="text-base italic">
|
|
||||||
Ich dokumentiere gerade weitere spannende Projekte aus den
|
|
||||||
Bereichen SaaS, E-Commerce und Systemarchitektur.
|
|
||||||
</LeadText>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section number="02" title="Philosophie" borderTop variant="gray">
|
{/* Coming Soon */}
|
||||||
<div className="max-w-3xl space-y-8">
|
<Section number="02" title="Kommt bald" borderTop>
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-4xl leading-tight">
|
<Card
|
||||||
Warum ich Case Studies zeige? <br />
|
variant="glass"
|
||||||
<span className="text-slate-200">
|
padding="large"
|
||||||
Weil Code mehr als Text ist.
|
techBorder
|
||||||
</span>
|
className="text-center relative overflow-hidden group"
|
||||||
</H3>
|
>
|
||||||
</Reveal>
|
<div className="relative z-10 space-y-6 py-8">
|
||||||
<Reveal delay={0.2}>
|
<div className="flex items-center justify-center gap-3">
|
||||||
<LeadText className="text-xl">
|
<div className="w-2 h-2 rounded-full bg-slate-300 animate-pulse" />
|
||||||
In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht
|
<Label className="text-slate-400">In Arbeit</Label>
|
||||||
um die technischen Entscheidungen, die ein Projekt erfolgreich
|
</div>
|
||||||
machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind
|
<H3 className="text-3xl text-slate-400">
|
||||||
keine Zufälle, sondern das Ergebnis von präziser Planung.
|
Weitere Case Studies in Kürze.
|
||||||
</LeadText>
|
</H3>
|
||||||
</Reveal>
|
<BodyText className="text-slate-400 max-w-md mx-auto">
|
||||||
</div>
|
Ich dokumentiere laufende Projekte – schauen Sie bald wieder
|
||||||
|
vorbei oder kontaktieren Sie mich direkt.
|
||||||
|
</BodyText>
|
||||||
|
<div className="pt-4">
|
||||||
|
<Button href="/contact" variant="outline">
|
||||||
|
Kontakt aufnehmen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,42 +1,95 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Reveal } from "../../src/components/Reveal";
|
|
||||||
import { PageHeader } from "../../src/components/PageHeader";
|
import { PageHeader } from "../../src/components/PageHeader";
|
||||||
|
import { Reveal } from "../../src/components/Reveal";
|
||||||
import { Section } from "../../src/components/Section";
|
import { Section } from "../../src/components/Section";
|
||||||
|
import { H3, LeadText, Label } from "../../src/components/Typography";
|
||||||
|
import { Card } from "../../src/components/Layout";
|
||||||
import { ContactForm } from "../../src/components/ContactForm";
|
import { ContactForm } from "../../src/components/ContactForm";
|
||||||
|
import { GradientMesh, AbstractCircuit } from "../../src/components/Effects";
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-12 py-12 md:py-24">
|
<div className="flex flex-col bg-white min-h-screen overflow-hidden relative">
|
||||||
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Projekt <br />
|
Kontakt<span className="text-slate-200">.</span>
|
||||||
<span className="text-slate-200">konfigurieren.</span>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description="Nutzen Sie den Konfigurator für eine erste Einschätzung oder schreiben Sie mir direkt eine Email."
|
description="Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich zeitnah bei Ihnen."
|
||||||
backLink={{ href: "/", label: "Zurück" }}
|
backLink={{ href: "/", label: "Zurück" }}
|
||||||
backgroundSymbol="?"
|
backgroundSymbol="@"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section title="Konfigurator" containerVariant="wide" className="!py-12">
|
<Section
|
||||||
<ContactForm />
|
borderTop
|
||||||
</Section>
|
effects={
|
||||||
|
<>
|
||||||
|
<GradientMesh variant="metallic" className="opacity-70" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24">
|
||||||
|
{/* Form */}
|
||||||
|
<div className="md:col-span-7">
|
||||||
|
<Reveal>
|
||||||
|
<Card
|
||||||
|
variant="glass"
|
||||||
|
padding="large"
|
||||||
|
className="relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<ContactForm />
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Section title="Direkt" className="!py-12">
|
{/* Sidebar */}
|
||||||
<div className="grid grid-cols-1 gap-24">
|
<div className="md:col-span-5 space-y-8">
|
||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.2}>
|
||||||
<div className="space-y-8">
|
<Card
|
||||||
<a href="mailto:marc@mintel.me" className="group block space-y-2">
|
variant="glass"
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-slate-300 group-hover:text-slate-900 transition-colors">
|
padding="normal"
|
||||||
Email
|
techBorder
|
||||||
</span>
|
className="group"
|
||||||
<p className="text-3xl md:text-6xl font-bold text-slate-900 border-b border-slate-100 group-hover:border-slate-900 transition-all duration-500 pb-6 tracking-tighter">
|
>
|
||||||
marc@mintel.me
|
<div className="space-y-4">
|
||||||
</p>
|
<div className="flex items-center gap-3">
|
||||||
</a>
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
</div>
|
<Label className="text-slate-900">Verfügbarkeit</Label>
|
||||||
</Reveal>
|
</div>
|
||||||
|
<LeadText className="text-lg text-slate-400">
|
||||||
|
Aktuell nehme ich Projekte für{" "}
|
||||||
|
<span className="text-slate-900 font-bold">Q2 2026</span>{" "}
|
||||||
|
an.
|
||||||
|
</LeadText>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.3}>
|
||||||
|
<Card variant="glass" padding="normal" className="group">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-slate-900">Direkt per E-Mail</Label>
|
||||||
|
<a
|
||||||
|
href="mailto:marc@mintel.me"
|
||||||
|
className="block text-xl md:text-2xl font-bold text-slate-900 hover:text-slate-400 transition-colors duration-500 border-b border-slate-100 hover:border-slate-400 pb-1"
|
||||||
|
>
|
||||||
|
marc@mintel.me
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={0.4}>
|
||||||
|
<div className="p-6 space-y-3 rounded-2xl border border-slate-50 bg-slate-50/30">
|
||||||
|
<Label className="text-slate-400">Antwortzeit</Label>
|
||||||
|
<H3 className="text-2xl text-slate-300">
|
||||||
|
<span className="text-slate-900">< 24h</span> an Werktagen.
|
||||||
|
</H3>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
@apply mb-1;
|
@apply mb-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
code:not([class*='language-']) {
|
code:not([class*="language-"]) {
|
||||||
@apply bg-slate-50 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] text-slate-800 border border-slate-100;
|
@apply bg-slate-50 px-1.5 py-0.5 rounded-md font-mono text-[0.9em] text-slate-800 border border-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,6 @@
|
|||||||
|
|
||||||
/* Components - Tailwind utility classes */
|
/* Components - Tailwind utility classes */
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
||||||
/* Legacy hooks required by tests */
|
/* Legacy hooks required by tests */
|
||||||
.file-example {
|
.file-example {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@@ -180,7 +179,8 @@
|
|||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm;
|
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm;
|
||||||
|
transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -266,7 +266,9 @@
|
|||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
border-color: #cbd5e1;
|
border-color: #cbd5e1;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgb(0 0 0 / 0.1),
|
||||||
|
0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduced motion support */
|
/* Reduced motion support */
|
||||||
@@ -284,7 +286,6 @@
|
|||||||
|
|
||||||
/* Print styles */
|
/* Print styles */
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
.floating-back-to-top,
|
.floating-back-to-top,
|
||||||
.reading-progress-bar {
|
.reading-progress-bar {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -343,27 +344,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-yellow {
|
.highlighter-yellow {
|
||||||
background: linear-gradient(135deg, rgba(255, 235, 59, 0.95) 0%, rgba(255, 213, 79, 0.95) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 235, 59, 0.95) 0%,
|
||||||
|
rgba(255, 213, 79, 0.95) 100%
|
||||||
|
);
|
||||||
color: #3f2f00;
|
color: #3f2f00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-pink {
|
.highlighter-pink {
|
||||||
background: linear-gradient(135deg, rgba(255, 167, 209, 0.95) 0%, rgba(255, 122, 175, 0.95) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 167, 209, 0.95) 0%,
|
||||||
|
rgba(255, 122, 175, 0.95) 100%
|
||||||
|
);
|
||||||
color: #3f0018;
|
color: #3f0018;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-green {
|
.highlighter-green {
|
||||||
background: linear-gradient(135deg, rgba(129, 199, 132, 0.95) 0%, rgba(102, 187, 106, 0.95) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(129, 199, 132, 0.95) 0%,
|
||||||
|
rgba(102, 187, 106, 0.95) 100%
|
||||||
|
);
|
||||||
color: #002f0a;
|
color: #002f0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-blue {
|
.highlighter-blue {
|
||||||
background: linear-gradient(135deg, rgba(226, 232, 240, 0.95) 0%, rgba(203, 213, 225, 0.95) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(226, 232, 240, 0.95) 0%,
|
||||||
|
rgba(203, 213, 225, 0.95) 100%
|
||||||
|
);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighter-tag:hover::before {
|
.highlighter-tag:hover::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -2px;
|
inset: -2px;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
@@ -385,7 +402,7 @@
|
|||||||
|
|
||||||
/* Marker Title Styles */
|
/* Marker Title Styles */
|
||||||
.marker-title::before {
|
.marker-title::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -0.15em;
|
left: -0.15em;
|
||||||
right: -0.15em;
|
right: -0.15em;
|
||||||
@@ -394,22 +411,23 @@
|
|||||||
border-radius: 0.18em;
|
border-radius: 0.18em;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(180deg,
|
180deg,
|
||||||
rgba(255, 255, 255, 0) 0%,
|
rgba(255, 255, 255, 0) 0%,
|
||||||
rgba(255, 255, 255, 0) 20%,
|
rgba(255, 255, 255, 0) 20%,
|
||||||
rgba(253, 230, 138, 0.70) 20%,
|
rgba(253, 230, 138, 0.7) 20%,
|
||||||
rgba(253, 230, 138, 0.70) 100%);
|
rgba(253, 230, 138, 0.7) 100%
|
||||||
|
);
|
||||||
|
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
transform:
|
transform: rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg))
|
||||||
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg));
|
skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg));
|
||||||
|
|
||||||
filter: saturate(1.05);
|
filter: saturate(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-title::after {
|
.marker-title::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -0.18em;
|
left: -0.18em;
|
||||||
right: -0.05em;
|
right: -0.05em;
|
||||||
@@ -418,27 +436,28 @@
|
|||||||
border-radius: 0.18em;
|
border-radius: 0.18em;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(90deg,
|
90deg,
|
||||||
rgba(253, 230, 138, 0.00) 0%,
|
rgba(253, 230, 138, 0) 0%,
|
||||||
rgba(253, 230, 138, 0.60) 8%,
|
rgba(253, 230, 138, 0.6) 8%,
|
||||||
rgba(253, 230, 138, 0.55) 60%,
|
rgba(253, 230, 138, 0.55) 60%,
|
||||||
rgba(253, 230, 138, 0.35) 100%);
|
rgba(253, 230, 138, 0.35) 100%
|
||||||
|
);
|
||||||
|
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
mix-blend-mode: multiply;
|
mix-blend-mode: multiply;
|
||||||
transform:
|
transform: rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg))
|
||||||
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) translateY(0.02em);
|
translateY(0.02em);
|
||||||
|
|
||||||
mask-image:
|
mask-image: linear-gradient(
|
||||||
linear-gradient(180deg,
|
180deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 1) 20%,
|
rgba(0, 0, 0, 1) 20%,
|
||||||
rgba(0, 0, 0, 1) 100%);
|
rgba(0, 0, 0, 1) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
|
||||||
.post-link:hover .marker-title::before,
|
.post-link:hover .marker-title::before,
|
||||||
.post-link:hover .marker-title::after {
|
.post-link:hover .marker-title::after {
|
||||||
filter: saturate(1.08) contrast(1.02);
|
filter: saturate(1.08) contrast(1.02);
|
||||||
@@ -480,7 +499,7 @@
|
|||||||
.mermaid-container .edgeLabel,
|
.mermaid-container .edgeLabel,
|
||||||
.mermaid-container .node text,
|
.mermaid-container .node text,
|
||||||
.mermaid-container tspan {
|
.mermaid-container tspan {
|
||||||
font-family: 'Inter', sans-serif !important;
|
font-family: "Inter", sans-serif !important;
|
||||||
fill: #334155 !important;
|
fill: #334155 !important;
|
||||||
color: #334155 !important;
|
color: #334155 !important;
|
||||||
stroke: none !important;
|
stroke: none !important;
|
||||||
@@ -532,7 +551,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.embed-wrapper:hover {
|
.embed-wrapper:hover {
|
||||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||||
|
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
border-color: #cbd5e1;
|
border-color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,20 +623,22 @@
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn[data-copied='true'] {
|
.copy-btn[data-copied="true"] {
|
||||||
color: #065f46;
|
color: #065f46;
|
||||||
background: rgba(16, 185, 129, 0.10);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
border-color: rgba(16, 185, 129, 0.35);
|
border-color: rgba(16, 185, 129, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prism.js syntax highlighting - light, low-noise */
|
/* Prism.js syntax highlighting - light, low-noise */
|
||||||
code[class*='language-'],
|
code[class*="language-"],
|
||||||
pre[class*='language-'],
|
pre[class*="language-"],
|
||||||
pre:has(code[class*='language-']) {
|
pre:has(code[class*="language-"]) {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
@@ -689,4 +712,269 @@ pre:has(code[class*='language-']) {
|
|||||||
.token.important,
|
.token.important,
|
||||||
.token.variable {
|
.token.variable {
|
||||||
color: #db2777;
|
color: #db2777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════
|
||||||
|
TECH AESTHETIC – Animation Keyframes
|
||||||
|
═══════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Gradient Mesh Blob Animations */
|
||||||
|
@keyframes gradient-blob-1 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translate(30px, -20px) scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translate(-15px, 25px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translate(20px, 15px) scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-blob-2 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
33% {
|
||||||
|
transform: translate(-25px, 15px) scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
66% {
|
||||||
|
transform: translate(20px, -20px) scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-blob-3 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: translate(15px, 10px) scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: translate(-10px, 20px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: translate(25px, -15px) scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: translate(-20px, -10px) scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Binary Stream Scroll */
|
||||||
|
@keyframes binary-scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Circuit Pulse (used for node glow effects) */
|
||||||
|
@keyframes circuit-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
box-shadow: 0 0 0 0 rgba(148, 163, 184, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
box-shadow: 0 0 12px 2px rgba(148, 163, 184, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Packet Flow */
|
||||||
|
@keyframes data-packet-flow {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(100vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech Border Glow */
|
||||||
|
@keyframes border-trace {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 200% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Fade-In Glow */
|
||||||
|
@keyframes section-glow {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tailwind-compatible animation classes */
|
||||||
|
.animate-gradient-blob-1 {
|
||||||
|
animation: gradient-blob-1 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient-blob-2 {
|
||||||
|
animation: gradient-blob-2 25s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient-blob-3 {
|
||||||
|
animation: gradient-blob-3 30s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-circuit-pulse {
|
||||||
|
animation: circuit-pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-border-trace {
|
||||||
|
animation: border-trace 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Utilities */
|
||||||
|
.glass {
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-subtle {
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(241, 245, 249, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-dark {
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech Border – animated gradient trace */
|
||||||
|
.tech-border {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-border::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(148, 163, 184, 0.3) 25%,
|
||||||
|
rgba(191, 206, 228, 0.2) 50%,
|
||||||
|
rgba(148, 163, 184, 0.3) 75%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: border-trace 4s linear infinite;
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
|
mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Noise texture overlay */
|
||||||
|
.noise-overlay::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.02;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════
|
||||||
|
ABSTRACT CIRCUIT – Trace Pulse Animations
|
||||||
|
═══════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
@keyframes tracePulse1 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 1280;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tracePulse2 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tracePulse3 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 1280;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tracePulse4 {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 440;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes junctionGlow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ComparisonRow,
|
ComparisonRow,
|
||||||
ConceptAutomation,
|
|
||||||
ConceptCode,
|
ConceptCode,
|
||||||
ConceptCommunication,
|
ConceptCommunication,
|
||||||
ConceptPrice,
|
ConceptPrice,
|
||||||
ConceptPrototyping,
|
ConceptPrototyping,
|
||||||
ConceptSystem,
|
|
||||||
ConceptWebsite,
|
ConceptWebsite,
|
||||||
DifferenceIllustration,
|
|
||||||
HeroArchitecture,
|
|
||||||
HeroMainIllustration,
|
|
||||||
} from "../src/components/Landing";
|
} from "../src/components/Landing";
|
||||||
import { Reveal } from "../src/components/Reveal";
|
import { Reveal } from "../src/components/Reveal";
|
||||||
import { Marker } from "../src/components/Marker";
|
|
||||||
import { Section } from "../src/components/Section";
|
import { Section } from "../src/components/Section";
|
||||||
import {
|
import {
|
||||||
H1,
|
H1,
|
||||||
@@ -22,172 +18,114 @@ import {
|
|||||||
MonoLabel,
|
MonoLabel,
|
||||||
Label,
|
Label,
|
||||||
} from "../src/components/Typography";
|
} from "../src/components/Typography";
|
||||||
import { BackgroundGrid, Card, Container } from "../src/components/Layout";
|
import { Card, Container } from "../src/components/Layout";
|
||||||
import { Button } from "../src/components/Button";
|
import { Button } from "../src/components/Button";
|
||||||
|
import { GradientMesh, CodeSnippet } from "../src/components/Effects";
|
||||||
import { IconList, IconListItem } from "../src/components/IconList";
|
import { IconList, IconListItem } from "../src/components/IconList";
|
||||||
|
import { HeroSection } from "../src/components/HeroSection";
|
||||||
|
import { GlitchText } from "../src/components/GlitchText";
|
||||||
|
import { Marker } from "../src/components/Marker";
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<BackgroundGrid />
|
{/* Dark Hero */}
|
||||||
|
<HeroSection />
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Rest of page on white */}
|
||||||
<section className="relative min-h-[80vh] flex items-center pt-24 md:pt-0">
|
|
||||||
<Container variant="narrow" className="relative">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24 items-center">
|
|
||||||
{/* Left Column */}
|
|
||||||
<div className="md:col-span-6 relative z-10">
|
|
||||||
<Reveal>
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-8 h-px bg-slate-900"></div>
|
|
||||||
<MonoLabel className="text-slate-900">
|
|
||||||
Digital Architect
|
|
||||||
</MonoLabel>
|
|
||||||
</div>
|
|
||||||
<H1 className="text-6xl md:text-8xl">
|
|
||||||
Websites <br />
|
|
||||||
<span className="text-slate-200">ohne Overhead.</span>
|
|
||||||
</H1>
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button href="#contact" variant="outline">
|
|
||||||
Projekt anfragen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column */}
|
{/* Section 02: The Promise – Streamlined */}
|
||||||
<div className="md:col-span-6 relative h-[400px] md:h-[600px] flex items-center justify-center">
|
<Section
|
||||||
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center scale-150">
|
number="02"
|
||||||
<HeroArchitecture className="w-full h-full" />
|
title="Das Versprechen"
|
||||||
</div>
|
borderTop
|
||||||
|
effects={<GradientMesh variant="metallic" className="opacity-70" />}
|
||||||
<Reveal
|
>
|
||||||
delay={0.2}
|
|
||||||
className="w-full h-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<div className="relative w-full h-full flex items-center justify-center pointer-events-none">
|
|
||||||
<HeroMainIllustration className="w-full h-full scale-110 md:scale-125 origin-center" />
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 02: The Promise */}
|
|
||||||
<Section number="02" title="Das Versprechen" borderTop>
|
|
||||||
<div className="space-y-16 relative">
|
<div className="space-y-16 relative">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="max-w-3xl">
|
<H3 className="max-w-3xl">
|
||||||
Schluss mit aufgeblähten Prozessen. <br />
|
Kein Agentur-Zirkus. <br />
|
||||||
<span className="text-slate-200">
|
<span className="text-slate-400">
|
||||||
Ich reduziere auf das Wesentliche.
|
Nur{" "}
|
||||||
|
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||||
|
Ergebnisse.
|
||||||
|
</Marker>
|
||||||
</span>
|
</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||||
<Reveal delay={0.1}>
|
{[
|
||||||
<div className="space-y-8">
|
{
|
||||||
<div className="flex items-center gap-4">
|
icon: <ConceptCommunication className="w-8 h-8" />,
|
||||||
<Label className="text-slate-900">Was ich biete</Label>
|
title: "Direkte Kommunikation",
|
||||||
</div>
|
text: "Sie sprechen mit dem Entwickler. Keine Stille Post, keine Umwege.",
|
||||||
<IconList className="space-y-6">
|
},
|
||||||
{[
|
{
|
||||||
{
|
icon: <ConceptPrototyping className="w-8 h-8" />,
|
||||||
text: "Direkte Kommunikation ohne Umwege",
|
title: "Schnelle Umsetzung",
|
||||||
icon: <ConceptCommunication className="w-12 h-12" />,
|
text: "Sichtbare Fortschritte in Tagen. Prototypen statt Konzeptpapiere.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Schnelle Prototypen statt langer Konzepte",
|
icon: <ConceptCode className="w-8 h-8" />,
|
||||||
icon: <ConceptPrototyping className="w-12 h-12" />,
|
title: "Sauberer Code",
|
||||||
},
|
text: "Maßgeschneiderte Architektur. Kein Baukasten, kein Plugin-Chaos.",
|
||||||
{
|
},
|
||||||
text: "Sauberer Code, der auch morgen noch läuft",
|
{
|
||||||
icon: <ConceptCode className="w-12 h-12" />,
|
icon: <ConceptPrice className="w-8 h-8" />,
|
||||||
},
|
title: "Klare Fixpreise",
|
||||||
{
|
text: "Volle Budgetsicherheit. Keine versteckten Kosten.",
|
||||||
text: "Fixpreise für volle Budgetsicherheit",
|
},
|
||||||
icon: <ConceptPrice className="w-12 h-12" />,
|
].map((item, i) => (
|
||||||
},
|
<Reveal key={i} delay={0.1 + i * 0.1}>
|
||||||
].map((item, i) => (
|
<Card
|
||||||
<IconListItem
|
variant="glass"
|
||||||
key={i}
|
padding="normal"
|
||||||
icon={item.icon}
|
techBorder
|
||||||
iconContainerClassName="mt-0"
|
className="group"
|
||||||
>
|
>
|
||||||
<LeadText className="text-xl">{item.text}</LeadText>
|
<div className="space-y-4 relative z-10">
|
||||||
</IconListItem>
|
<div className="w-12 h-12 rounded-xl bg-slate-50 border border-slate-100 flex items-center justify-center group-hover:scale-110 transition-transform duration-500">
|
||||||
))}
|
{item.icon}
|
||||||
</IconList>
|
</div>
|
||||||
</div>
|
<Label className="text-slate-900">{item.title}</Label>
|
||||||
</Reveal>
|
<BodyText className="text-slate-500">{item.text}</BodyText>
|
||||||
|
</div>
|
||||||
<Reveal delay={0.2}>
|
</Card>
|
||||||
<div className="space-y-8 opacity-40 hover:opacity-100 transition-opacity duration-700">
|
</Reveal>
|
||||||
<div className="flex items-center gap-4">
|
))}
|
||||||
<Label>Was ich nicht mache</Label>
|
|
||||||
</div>
|
|
||||||
<IconList className="space-y-4">
|
|
||||||
{[
|
|
||||||
"Endlose Workshops ohne Ergebnis",
|
|
||||||
"PowerPoint-Schlachten",
|
|
||||||
"Outsourcing an Billig-Anbieter",
|
|
||||||
"Wartungsverträge mit versteckten Kosten",
|
|
||||||
].map((item, i) => (
|
|
||||||
<IconListItem
|
|
||||||
key={i}
|
|
||||||
bullet
|
|
||||||
className="line-through decoration-slate-200"
|
|
||||||
iconClassName="opacity-20"
|
|
||||||
>
|
|
||||||
<LeadText className="text-slate-400 text-lg">
|
|
||||||
{item}
|
|
||||||
</LeadText>
|
|
||||||
</IconListItem>
|
|
||||||
))}
|
|
||||||
</IconList>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 03: The Difference */}
|
{/* Section 03: The Difference – Visual Comparison */}
|
||||||
<Section number="03" title="Der Unterschied" variant="white" borderTop>
|
<Section number="03" title="Der Unterschied" variant="white" borderTop>
|
||||||
<div className="space-y-16 relative">
|
<div className="space-y-16 relative">
|
||||||
<div className="flex flex-col md:flex-row gap-12 items-center">
|
<Reveal>
|
||||||
<Reveal className="flex-1">
|
<H3 className="max-w-3xl">
|
||||||
<LeadText className="text-2xl md:text-3xl leading-tight max-w-2xl relative z-10 text-slate-400">
|
Ich arbeite für das Ergebnis, <br />
|
||||||
Ich arbeite nicht gegen die Zeit, sondern{" "}
|
<span className="text-slate-400">
|
||||||
<span className="text-slate-900">für das Ergebnis.</span> Mein
|
nicht gegen die{" "}
|
||||||
Fokus liegt auf der Umsetzung, nicht auf der Verwaltung von
|
<Marker delay={0.4} color="rgba(148,163,184,0.1)">
|
||||||
Prozessen.
|
Uhr.
|
||||||
</LeadText>
|
</Marker>
|
||||||
</Reveal>
|
</span>
|
||||||
<Reveal delay={0.2} className="w-full md:w-72 shrink-0">
|
</H3>
|
||||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
</Reveal>
|
||||||
<DifferenceIllustration className="w-full h-auto grayscale opacity-50" />
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8 relative z-20">
|
<div className="grid grid-cols-1 gap-8 relative z-20">
|
||||||
<ComparisonRow
|
<ComparisonRow
|
||||||
negativeLabel="Klassisch"
|
negativeLabel="Klassisch"
|
||||||
negativeText="Lange Planungsphasen und abstrakte Konzepte."
|
negativeText="Wochen in Planung, bevor eine einzige Zeile Code geschrieben wird."
|
||||||
positiveLabel="Mein Weg"
|
positiveLabel="Mein Weg"
|
||||||
positiveText="Schnelle Prototypen. Sie sehen Fortschritt in Tagen."
|
positiveText="Schnelle Prototypen. Ergebnisse in Tagen, nicht Monaten."
|
||||||
delay={0.1}
|
delay={0.1}
|
||||||
/>
|
/>
|
||||||
<ComparisonRow
|
<ComparisonRow
|
||||||
negativeLabel="Klassisch"
|
negativeLabel="Klassisch"
|
||||||
negativeText="Komplexe Preisstrukturen und versteckte Kosten."
|
negativeText="Unvorhersehbare Kosten durch Stundenabrechnungen."
|
||||||
positiveLabel="Mein Weg"
|
positiveLabel="Mein Weg"
|
||||||
positiveText="Klare Fixpreise. Volle Kostentransparenz."
|
positiveText="Fixpreise. Sie wissen von Anfang an, was es kostet."
|
||||||
reverse
|
reverse
|
||||||
delay={0.2}
|
delay={0.2}
|
||||||
/>
|
/>
|
||||||
@@ -196,43 +134,40 @@ export default function LandingPage() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 04: Target Group */}
|
{/* Section 04: Target Group */}
|
||||||
<Section number="04" title="Zielgruppe" borderTop>
|
<Section number="04" title="Für wen" borderTop>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<Card variant="dark" padding="normal" className="group">
|
<Card variant="glass" padding="normal" techBorder className="group">
|
||||||
<div className="space-y-6 relative overflow-hidden">
|
<div className="space-y-6 relative overflow-hidden">
|
||||||
<div className="w-16 h-16 bg-white/5 rounded-xl flex items-center justify-center border border-white/10">
|
<div className="w-16 h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
|
||||||
<ConceptPrice className="w-8 h-8" />
|
<ConceptPrice className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<H3 className="text-white text-3xl">
|
<H3 className="text-3xl">
|
||||||
Unternehmer & <br />
|
Unternehmer & <br />
|
||||||
Geschäftsführer
|
Geschäftsführer
|
||||||
</H3>
|
</H3>
|
||||||
<LeadText className="text-slate-400 text-lg">
|
<LeadText className="text-slate-400 text-lg">
|
||||||
"Ich brauche eine Lösung, die funktioniert. Ich habe keine
|
Sie wollen eine Website, die funktioniert – ohne sich mit
|
||||||
Zeit für technische Details."
|
Technik beschäftigen zu müssen.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-8 border-t border-white/5 mt-8">
|
<div className="pt-8 border-t border-slate-50 mt-8">
|
||||||
<Label className="group-hover:text-white transition-colors">
|
<Label className="group-hover:text-slate-900 transition-colors">
|
||||||
Perfekt für Sie
|
Perfekt für Sie
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<Card variant="white" padding="normal" className="group">
|
<Card variant="glass" padding="normal" techBorder className="group">
|
||||||
<div className="space-y-6 relative overflow-hidden">
|
<div className="space-y-6 relative overflow-hidden">
|
||||||
<div className="w-16 h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
|
|
||||||
<ConceptWebsite className="w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
<H3 className="text-3xl">
|
<H3 className="text-3xl">
|
||||||
Marketing & <br />
|
Marketing & <br />
|
||||||
Vertrieb
|
Vertrieb
|
||||||
</H3>
|
</H3>
|
||||||
<LeadText className="text-slate-400 text-lg">
|
<LeadText className="text-slate-400 text-lg">
|
||||||
"Wir brauchen Landingpages und Tools, um unsere Ziele zu
|
Sie brauchen Landingpages und Tools, die Ergebnisse liefern.
|
||||||
erreichen. Schnell und zuverlässig."
|
Schnell und zuverlässig.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-8 border-t border-slate-50 mt-8">
|
<div className="pt-8 border-t border-slate-50 mt-8">
|
||||||
@@ -245,65 +180,93 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Section 05: Services */}
|
{/* Section 05: Leistungen — Interactive Service Rows */}
|
||||||
<Section number="05" title="Leistungen" variant="gray" borderTop>
|
<Section number="05" title="Leistungen" variant="gray" borderTop>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative z-20">
|
<div className="space-y-0 relative z-20">
|
||||||
<Reveal delay={0.1}>
|
{[
|
||||||
<Card variant="white" padding="small" className="group">
|
{
|
||||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
num: "01",
|
||||||
<ConceptWebsite className="w-8 h-8" />
|
binary: "00000001",
|
||||||
</div>
|
title: "Websites",
|
||||||
<div className="space-y-4">
|
text: "High-Performance Websites mit maßgeschneiderter Architektur. Von der Konzeption bis zum Go-Live — individuell, schnell, messbar.",
|
||||||
<H3 className="text-2xl">Websites</H3>
|
tags: ["Next.js", "React", "TypeScript", "Performance"],
|
||||||
<BodyText>
|
href: "/websites",
|
||||||
High-Performance Websites. Maßgeschneiderte Architektur statt
|
},
|
||||||
Baukasten.
|
{
|
||||||
</BodyText>
|
num: "02",
|
||||||
<div className="pt-4">
|
binary: "00000010",
|
||||||
<a
|
title: "Systeme",
|
||||||
href="/websites"
|
text: "Web-Applikationen und interne Tools, wenn Standard-Software nicht reicht. Dashboards, Portale, Automatisierungen.",
|
||||||
className="text-[10px] font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-100 pb-1 hover:border-slate-900 transition-all"
|
tags: ["Full-Stack", "APIs", "Datenbanken", "Auth"],
|
||||||
>
|
href: "/contact",
|
||||||
Details
|
},
|
||||||
</a>
|
{
|
||||||
|
num: "03",
|
||||||
|
binary: "00000011",
|
||||||
|
title: "Automatisierung",
|
||||||
|
text: "Verbindung von Tools, automatische Prozesse, Daten-Synchronisation. Weniger manuelle Arbeit, mehr Effizienz.",
|
||||||
|
tags: ["CI/CD", "Workflows", "Integrationen", "Monitoring"],
|
||||||
|
href: "/contact",
|
||||||
|
},
|
||||||
|
].map((service, i) => (
|
||||||
|
<Reveal key={i} delay={0.1 + i * 0.15}>
|
||||||
|
<div className="group py-12 md:py-16 border-b border-slate-100 last:border-b-0 cursor-pointer transition-all duration-500">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-start gap-8 md:gap-16">
|
||||||
|
{/* Number + Binary */}
|
||||||
|
<div className="md:w-32 shrink-0">
|
||||||
|
<span className="text-5xl md:text-6xl font-black text-slate-100 group-hover:text-slate-200 transition-colors duration-500 tracking-tighter block leading-none">
|
||||||
|
{service.num}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[8px] font-mono text-slate-200 tracking-[0.3em] mt-2 block select-none group-hover:text-blue-300 transition-colors duration-700"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{service.binary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 space-y-6">
|
||||||
|
<H3 className="text-3xl md:text-4xl group-hover:translate-x-2 transition-transform duration-500">
|
||||||
|
<GlitchText
|
||||||
|
trigger="inView"
|
||||||
|
delay={0.2 + i * 0.15}
|
||||||
|
duration={0.6}
|
||||||
|
>
|
||||||
|
{service.title}
|
||||||
|
</GlitchText>
|
||||||
|
</H3>
|
||||||
|
<BodyText className="text-slate-400 max-w-xl group-hover:text-slate-500 transition-colors duration-500">
|
||||||
|
{service.text}
|
||||||
|
</BodyText>
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{service.tags.map((tag, j) => (
|
||||||
|
<span
|
||||||
|
key={j}
|
||||||
|
className="px-3 py-1 text-[9px] font-mono uppercase tracking-widest text-slate-400 border border-slate-100 rounded-full bg-white/50 group-hover:border-slate-200 group-hover:text-slate-500 transition-all duration-500"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
<div className="md:self-center shrink-0">
|
||||||
|
<Button
|
||||||
|
href={service.href}
|
||||||
|
variant="ghost"
|
||||||
|
size="normal"
|
||||||
|
showArrow
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Reveal>
|
||||||
</Reveal>
|
))}
|
||||||
|
|
||||||
<Reveal delay={0.3}>
|
|
||||||
<Card
|
|
||||||
variant="white"
|
|
||||||
padding="small"
|
|
||||||
className="group mt-8 md:mt-0"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
|
||||||
<ConceptSystem className="w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<H3 className="text-2xl">Systeme</H3>
|
|
||||||
<BodyText>
|
|
||||||
Web-Applikationen, Portale, interne Tools. Wenn Standard an
|
|
||||||
Grenzen stößt.
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={0.5}>
|
|
||||||
<Card variant="white" padding="small" className="group">
|
|
||||||
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
|
|
||||||
<ConceptAutomation className="w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<H3 className="text-2xl">Automatisierung</H3>
|
|
||||||
<BodyText>
|
|
||||||
Verbindung von Tools, automatische Prozesse,
|
|
||||||
Daten-Synchronisation.
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -314,26 +277,26 @@ export default function LandingPage() {
|
|||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
<H1 className="text-6xl md:text-8xl">
|
<H1 className="text-6xl md:text-8xl">
|
||||||
Lassen Sie uns <br />
|
Lassen Sie uns <br />
|
||||||
<span className="text-slate-200">starten.</span>
|
<span className="text-slate-400">starten.</span>
|
||||||
</H1>
|
</H1>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-16 items-start relative z-10">
|
<div className="flex flex-col md:flex-row gap-16 items-start relative z-10">
|
||||||
<div className="space-y-8 flex-1">
|
<div className="space-y-8 flex-1">
|
||||||
<LeadText className="text-2xl md:text-3xl text-slate-400">
|
<LeadText className="text-2xl md:text-3xl text-slate-400">
|
||||||
Schreiben Sie mir kurz, worum es geht. Ich melde mich{" "}
|
Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "}
|
||||||
<span className="text-slate-900">zeitnah</span> bei Ihnen.
|
<span className="text-slate-900 border-b-2 border-slate-900/10">
|
||||||
|
<Marker color="rgba(255,235,59,0.5)">zeitnah</Marker>
|
||||||
|
</span>{" "}
|
||||||
|
bei Ihnen.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<a
|
<Button href="/contact" size="large">
|
||||||
href="/contact"
|
|
||||||
className="inline-block text-3xl md:text-5xl font-bold text-slate-900 hover:text-slate-400 transition-all duration-700 border-b-2 border-slate-900 hover:border-slate-200 pb-2"
|
|
||||||
>
|
|
||||||
Projekt anfragen
|
Projekt anfragen
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full md:w-72 space-y-6 p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
<div className="w-full md:w-72 space-y-6 p-6 glass rounded-2xl">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
<Label className="text-slate-900">Verfügbarkeit</Label>
|
<Label className="text-slate-900">Verfügbarkeit</Label>
|
||||||
|
|||||||
@@ -10,98 +10,129 @@ import {
|
|||||||
LayerSeparation,
|
LayerSeparation,
|
||||||
DirectService,
|
DirectService,
|
||||||
TaskDone,
|
TaskDone,
|
||||||
ConceptAutomation,
|
|
||||||
ConceptCode,
|
|
||||||
ConceptCommunication,
|
|
||||||
ConceptPrototyping,
|
|
||||||
ConceptSystem,
|
|
||||||
ConceptTarget,
|
|
||||||
} from "../../src/components/Landing";
|
} from "../../src/components/Landing";
|
||||||
import { Check } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
H2,
|
|
||||||
H3,
|
H3,
|
||||||
H4,
|
H4,
|
||||||
LeadText,
|
LeadText,
|
||||||
BodyText,
|
BodyText,
|
||||||
Label,
|
Label,
|
||||||
} from "../../src/components/Typography";
|
} from "../../src/components/Typography";
|
||||||
import { BackgroundGrid, Card } from "../../src/components/Layout";
|
import { Card } from "../../src/components/Layout";
|
||||||
import { MotionButton } from "../../src/components/Button";
|
import { Button } from "../../src/components/Button";
|
||||||
import { IconList, IconListItem } from "../../src/components/IconList";
|
import { IconList, IconListItem } from "../../src/components/IconList";
|
||||||
|
import {
|
||||||
|
GradientMesh,
|
||||||
|
CodeSnippet,
|
||||||
|
AbstractCircuit,
|
||||||
|
} from "../../src/components/Effects";
|
||||||
|
import { Marker } from "../../src/components/Marker";
|
||||||
|
|
||||||
export default function WebsitesPage() {
|
export default function WebsitesPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||||
<BackgroundGrid />
|
<AbstractCircuit />
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Websites, die <br />
|
Websites, die <br />
|
||||||
<span className="text-slate-200">einfach funktionieren.</span>
|
<span className="text-slate-400">
|
||||||
|
<Marker color="rgba(255,235,59,0.5)">
|
||||||
|
einfach funktionieren.
|
||||||
|
</Marker>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
description="Keine Baukästen, keine Plugins, kein Overhead. Nur sauberer Code und maximale Performance."
|
description="Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur für maximale Performance."
|
||||||
backLink={{ href: "/", label: "Zurück" }}
|
backLink={{ href: "/", label: "Zurück" }}
|
||||||
backgroundSymbol="W"
|
backgroundSymbol="W"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Intro / Problem */}
|
{/* 01: Architektur – WIE ich baue */}
|
||||||
<Section
|
<Section
|
||||||
number="01"
|
number="01"
|
||||||
title="Der Ansatz"
|
title="Architektur"
|
||||||
borderTop
|
borderTop
|
||||||
illustration={<SystemArchitecture className="w-24 h-24" />}
|
illustration={<SystemArchitecture className="w-24 h-24" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Ich baue Websites wie Systeme – <br />
|
Systeme, nicht Broschüren. <br />
|
||||||
<span className="text-slate-200">nicht wie Broschüren.</span>
|
<span className="text-slate-400">
|
||||||
|
Jede Website ist Ingenieursarbeit.
|
||||||
|
</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
||||||
Eine Website ist kein Flyer. Sie ist ein{" "}
|
Ich entwickle Websites von Grund auf – mit modernen Frameworks,
|
||||||
<span className="text-slate-900">Werkzeug</span>, das jeden Tag
|
eigener Infrastruktur und einem Deployment-Prozess, der{" "}
|
||||||
arbeitet. Deshalb baue ich sie stabil, schnell und wartungsfrei.
|
<span className="text-slate-900">
|
||||||
|
<Marker delay={0.2} color="rgba(148,163,184,0.1)">
|
||||||
|
automatisiert und reproduzierbar
|
||||||
|
</Marker>
|
||||||
|
</span>{" "}
|
||||||
|
ist.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Tech Stack Visual */}
|
||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.4}>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-8">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: "Stabil", icon: ConceptSystem },
|
{ label: "Next.js", sub: "Framework" },
|
||||||
{ label: "Schnell", icon: ConceptAutomation },
|
{ label: "TypeScript", sub: "Sprache" },
|
||||||
{ label: "Wartungsfrei", icon: ConceptCode },
|
{ label: "Docker", sub: "Infrastruktur" },
|
||||||
{ label: "Sicher", icon: ConceptTarget },
|
{ label: "Directus", sub: "CMS" },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="flex flex-col gap-3 group">
|
<Card
|
||||||
<div className="w-12 h-12 rounded-xl bg-slate-50 flex items-center justify-center border border-slate-100 group-hover:scale-110 transition-transform duration-500">
|
key={i}
|
||||||
<item.icon className="w-6 h-6" />
|
variant="glass"
|
||||||
|
padding="small"
|
||||||
|
techBorder
|
||||||
|
className="group text-center"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-slate-900 text-sm">
|
||||||
|
{item.label}
|
||||||
|
</Label>
|
||||||
|
<span className="block text-[9px] font-mono text-slate-300 uppercase tracking-widest">
|
||||||
|
{item.sub}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Label className="text-slate-900">{item.label}</Label>
|
</Card>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Decorative Code Snippet */}
|
||||||
|
<Reveal delay={0.6}>
|
||||||
|
<div className="max-w-md opacity-70">
|
||||||
|
<CodeSnippet variant="code" />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Speed */}
|
{/* 02: Performance */}
|
||||||
<Section
|
<Section
|
||||||
number="02"
|
number="02"
|
||||||
title="Performance"
|
title="Performance"
|
||||||
borderTop
|
borderTop
|
||||||
variant="gray"
|
variant="gray"
|
||||||
illustration={<SpeedPerformance className="w-24 h-24" />}
|
illustration={<SpeedPerformance className="w-24 h-24" />}
|
||||||
|
effects={<GradientMesh variant="metallic" className="opacity-60" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Geschwindigkeit ist <br />
|
Geschwindigkeit ist <br />
|
||||||
<span className="text-slate-200">
|
<span className="text-slate-400">
|
||||||
kein Extra. Sie ist Standard.
|
<Marker delay={0.3} color="rgba(148,163,184,0.15)">
|
||||||
|
kein Extra. Sie ist Standard.
|
||||||
|
</Marker>
|
||||||
</span>
|
</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -109,19 +140,18 @@ export default function WebsitesPage() {
|
|||||||
<div className="md:col-span-7 space-y-8">
|
<div className="md:col-span-7 space-y-8">
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-xl text-slate-400">
|
<LeadText className="text-xl text-slate-400">
|
||||||
Viele Websites sind langsam, weil sie zusammengeklickt sind.
|
Jede Seite wird vorab gerendert und über ein CDN ausgeliefert.
|
||||||
Meine sind schnell, weil sie{" "}
|
Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "}
|
||||||
<span className="text-slate-900">von Grund auf</span>{" "}
|
<span className="text-slate-900">Reproduzierbar.</span>
|
||||||
entwickelt wurden.
|
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.4}>
|
||||||
<IconList className="space-y-4">
|
<IconList className="space-y-4">
|
||||||
{[
|
{[
|
||||||
"Seiten laden ohne Verzögerung",
|
"Server-Side Rendering für sofortige Inhalte",
|
||||||
"Optimiert für Suchmaschinen (SEO)",
|
"Automatische Bild-Optimierung (WebP, AVIF)",
|
||||||
"Bessere Nutzererfahrung",
|
"Lighthouse-Score 90+ als Mindeststandard",
|
||||||
"Höhere Conversion-Rates",
|
"Core Web Vitals im grünen Bereich",
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<IconListItem key={i} bullet>
|
<IconListItem key={i} bullet>
|
||||||
<LeadText className="text-lg md:text-xl">{item}</LeadText>
|
<LeadText className="text-lg md:text-xl">{item}</LeadText>
|
||||||
@@ -133,14 +163,18 @@ export default function WebsitesPage() {
|
|||||||
<div className="md:col-span-5">
|
<div className="md:col-span-5">
|
||||||
<Reveal delay={0.6}>
|
<Reveal delay={0.6}>
|
||||||
<Card
|
<Card
|
||||||
variant="white"
|
variant="glass"
|
||||||
padding="normal"
|
padding="normal"
|
||||||
|
techBorder
|
||||||
className="text-center group"
|
className="text-center group"
|
||||||
>
|
>
|
||||||
<div className="text-7xl md:text-8xl font-bold text-slate-900 tracking-tighter group-hover:scale-110 transition-transform duration-700">
|
<div className="text-7xl md:text-8xl font-bold text-slate-900 tracking-tighter group-hover:scale-110 transition-transform duration-700">
|
||||||
90+
|
90+
|
||||||
</div>
|
</div>
|
||||||
<Label className="mt-4">Pagespeed Score</Label>
|
<Label className="mt-4">Lighthouse Score</Label>
|
||||||
|
<span className="block text-[9px] font-mono text-slate-300 mt-2 tracking-wider">
|
||||||
|
PERFORMANCE · ACCESSIBILITY · SEO
|
||||||
|
</span>
|
||||||
</Card>
|
</Card>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,10 +182,10 @@ export default function WebsitesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* No Maintenance */}
|
{/* 03: Code-Qualität */}
|
||||||
<Section
|
<Section
|
||||||
number="03"
|
number="03"
|
||||||
title="Technik"
|
title="Code"
|
||||||
borderTop
|
borderTop
|
||||||
illustration={<SolidFoundation className="w-24 h-24" />}
|
illustration={<SolidFoundation className="w-24 h-24" />}
|
||||||
>
|
>
|
||||||
@@ -159,91 +193,107 @@ export default function WebsitesPage() {
|
|||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Keine Plugins. <br />
|
Keine Plugins. <br />
|
||||||
<span className="text-slate-200">Keine Abhängigkeiten.</span>
|
<span className="text-slate-400">Keine Abhängigkeiten.</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
||||||
Ich nutze keine Baukästen, die sich selbst zerstören. Ihre Website
|
Ihre Website besteht aus{" "}
|
||||||
besteht aus <span className="text-slate-900">sauberem Code</span>,
|
<span className="text-slate-900">Ihrem Code</span>. Kein
|
||||||
der Ihnen gehört.
|
WordPress, kein Wix, keine Blackbox. Alles versioniert, alles
|
||||||
|
nachvollziehbar.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={0.4}>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
|
{/* Git Branch Visualization */}
|
||||||
<Card variant="white" padding="normal" className="group">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="space-y-4">
|
<Reveal delay={0.4}>
|
||||||
<Label className="text-slate-900 mb-2">Code Qualität</Label>
|
<CodeSnippet variant="git" />
|
||||||
<H4 className="text-2xl">Langlebigkeit</H4>
|
</Reveal>
|
||||||
<BodyText>
|
<Reveal delay={0.5}>
|
||||||
Modernste Web-Technologien für maximale Performance und
|
<div className="space-y-6">
|
||||||
Wartbarkeit.
|
<Card variant="glass" padding="normal" className="group">
|
||||||
</BodyText>
|
<div className="space-y-2">
|
||||||
</div>
|
<Label className="text-slate-900">Versionskontrolle</Label>
|
||||||
</Card>
|
<BodyText>
|
||||||
<Card variant="white" padding="normal" className="group">
|
Jede Änderung ist dokumentiert. Rollbacks in Sekunden.
|
||||||
<div className="space-y-4">
|
Kein „wer hat das kaputt gemacht?".
|
||||||
<Label className="text-slate-900 mb-2">Sicherheit</Label>
|
</BodyText>
|
||||||
<H4 className="text-2xl">Resilienz</H4>
|
</div>
|
||||||
<BodyText>
|
</Card>
|
||||||
Minimale Angriffsfläche durch Verzicht auf unnötige
|
<Card variant="glass" padding="normal" className="group">
|
||||||
Drittanbieter-Software.
|
<div className="space-y-2">
|
||||||
</BodyText>
|
<Label className="text-slate-900">
|
||||||
</div>
|
Automatisches Deployment
|
||||||
</Card>
|
</Label>
|
||||||
</div>
|
<BodyText>
|
||||||
</Reveal>
|
Code wird geprüft, getestet und automatisch live
|
||||||
|
geschaltet. Ohne manuellen Eingriff.
|
||||||
|
</BodyText>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Content/Tech Separation */}
|
{/* 04: Content-System */}
|
||||||
<Section
|
<Section
|
||||||
number="04"
|
number="04"
|
||||||
title="Inhalte"
|
title="Inhalte"
|
||||||
borderTop
|
borderTop
|
||||||
variant="gray"
|
variant="gray"
|
||||||
illustration={<LayerSeparation className="w-24 h-24" />}
|
illustration={<LayerSeparation className="w-24 h-24" />}
|
||||||
|
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
||||||
Inhalte pflegen <br />
|
Inhalte pflegen <br />
|
||||||
<span className="text-slate-200">ohne Angst.</span>
|
<span className="text-slate-400">ohne Angst.</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-start">
|
||||||
<div className="md:col-span-7">
|
<div className="md:col-span-7">
|
||||||
<Reveal delay={0.2}>
|
<Reveal delay={0.2}>
|
||||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||||
Sie können Texte und Bilder selbst anpassen, ohne das Design
|
Technik und Inhalt sind{" "}
|
||||||
oder die Technik zu gefährden. Ein{" "}
|
<span className="text-slate-900">
|
||||||
<span className="text-slate-900">intuitives System</span>{" "}
|
<Marker color="rgba(255,235,59,0.5)">
|
||||||
sorgt dafür, dass alles an seinem Platz bleibt.
|
strikt getrennt
|
||||||
|
</Marker>
|
||||||
|
</span>
|
||||||
|
. Sie bearbeiten Texte und Bilder in einem intuitiven System –
|
||||||
|
das Design bleibt geschützt.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-5">
|
<div className="md:col-span-5">
|
||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.4}>
|
||||||
<Card variant="white" padding="normal" className="space-y-8">
|
<Card
|
||||||
<div className="space-y-4">
|
variant="glass"
|
||||||
<Label>Ihre Freiheit</Label>
|
padding="normal"
|
||||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900">
|
techBorder
|
||||||
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center">
|
className="space-y-6"
|
||||||
<Check className="w-3 h-3 text-white" />
|
>
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
Inhalte flexibel verwalten
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||||
|
<Label className="text-slate-900">Sie dürfen</Label>
|
||||||
</div>
|
</div>
|
||||||
|
<BodyText className="font-medium">
|
||||||
|
Texte, Bilder und Inhalte frei bearbeiten.
|
||||||
|
</BodyText>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 opacity-30">
|
<div className="space-y-3 opacity-40">
|
||||||
<Label>Mein Schutz</Label>
|
<div className="flex items-center gap-3">
|
||||||
<div className="space-y-2">
|
<div className="w-2 h-2 rounded-full bg-slate-300"></div>
|
||||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">
|
<Label>Geschützt</Label>
|
||||||
Design-Chaos
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">
|
|
||||||
Technische Fehler
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<BodyText className="line-through">
|
||||||
|
Design, Layout, Code-Struktur.
|
||||||
|
</BodyText>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -252,90 +302,34 @@ export default function WebsitesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Simple Changes */}
|
{/* 05: Was Sie bekommen */}
|
||||||
<Section
|
<Section
|
||||||
number="05"
|
number="05"
|
||||||
title="Service"
|
|
||||||
borderTop
|
|
||||||
illustration={<DirectService className="w-24 h-24" />}
|
|
||||||
>
|
|
||||||
<div className="space-y-12">
|
|
||||||
<Reveal>
|
|
||||||
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
|
|
||||||
Änderungen sind <br />
|
|
||||||
<span className="text-slate-200">Teil des Konzepts.</span>
|
|
||||||
</H3>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={0.2}>
|
|
||||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
|
|
||||||
Ihr Business entwickelt sich weiter, Ihre Website auch. <br />
|
|
||||||
Keine komplizierten Prozesse, sondern{" "}
|
|
||||||
<span className="text-slate-900">direkte Umsetzung</span> Ihrer
|
|
||||||
Ideen.
|
|
||||||
</LeadText>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={0.4}>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
|
|
||||||
<Card
|
|
||||||
variant="white"
|
|
||||||
padding="normal"
|
|
||||||
className="group hover:border-slate-900"
|
|
||||||
>
|
|
||||||
<ConceptCommunication className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<H4 className="text-2xl">Direkter Draht</H4>
|
|
||||||
<BodyText>
|
|
||||||
Sie sprechen direkt mit dem Entwickler. Keine Stille Post.
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
variant="white"
|
|
||||||
padding="normal"
|
|
||||||
className="group hover:border-slate-900"
|
|
||||||
>
|
|
||||||
<ConceptPrototyping className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<H4 className="text-2xl">Agile Anpassung</H4>
|
|
||||||
<BodyText>
|
|
||||||
Schnelle Iterationen statt langer Wartezeiten.
|
|
||||||
</BodyText>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* Result */}
|
|
||||||
<Section
|
|
||||||
number="06"
|
|
||||||
title="Ergebnis"
|
title="Ergebnis"
|
||||||
borderTop
|
borderTop
|
||||||
variant="gray"
|
|
||||||
illustration={<TaskDone className="w-24 h-24" />}
|
illustration={<TaskDone className="w-24 h-24" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<H3 className="text-4xl md:text-6xl tracking-tighter">
|
<H3 className="text-4xl md:text-6xl tracking-tighter">
|
||||||
Eine Website, die <br />
|
Was Sie konkret <br />
|
||||||
<span className="text-slate-200">einfach läuft.</span>
|
<span className="text-slate-400">bekommen.</span>
|
||||||
</H3>
|
</H3>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
title: "Kein Overhead",
|
title: "Ihr Code",
|
||||||
desc: "Fokus auf das, was Ihre Kunden wirklich brauchen.",
|
desc: "Vollständiger Quellcode, versioniert auf GitHub. Kein Vendor Lock-in.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Volle Kontrolle",
|
title: "Ihre Infrastruktur",
|
||||||
desc: "Der Code gehört Ihnen, ohne Vendor Lock-in.",
|
desc: "Docker-Container, CI/CD-Pipeline, automatisches Deployment.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Echte Performance",
|
title: "Ihr CMS",
|
||||||
desc: "Messbare Geschwindigkeit für bessere Ergebnisse.",
|
desc: "Eigenes Content-Management-System. Volle Kontrolle über Ihre Inhalte.",
|
||||||
},
|
},
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<Reveal key={i} delay={i * 0.1}>
|
<Reveal key={i} delay={i * 0.1}>
|
||||||
@@ -353,12 +347,12 @@ export default function WebsitesPage() {
|
|||||||
<Reveal delay={0.4}>
|
<Reveal delay={0.4}>
|
||||||
<div className="pt-16 border-t border-slate-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-8">
|
<div className="pt-16 border-t border-slate-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Bereit für mehr?</Label>
|
<Label>Bereit?</Label>
|
||||||
<LeadText className="text-2xl">
|
<LeadText className="text-2xl">
|
||||||
Lassen Sie uns über Ihr nächstes Projekt sprechen.
|
Lassen Sie uns über Ihr Projekt sprechen.
|
||||||
</LeadText>
|
</LeadText>
|
||||||
</div>
|
</div>
|
||||||
<MotionButton href="/contact">Projekt anfragen</MotionButton>
|
<Button href="/contact">Projekt anfragen</Button>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/case-studies/klz',
|
||||||
|
destination: '/case-studies/klz-cables',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withMintelConfig(nextConfig);
|
export default withMintelConfig(nextConfig);
|
||||||
|
|||||||
@@ -1,67 +1,166 @@
|
|||||||
import * as React from 'react';
|
"use client";
|
||||||
import { ArrowRight } from 'lucide-react';
|
|
||||||
import { motion } from 'framer-motion';
|
import * as React from "react";
|
||||||
import Link from 'next/link';
|
import { motion } from "framer-motion";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
href: string;
|
href: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: 'primary' | 'outline';
|
variant?: "primary" | "outline" | "ghost";
|
||||||
|
size?: "normal" | "large";
|
||||||
className?: string;
|
className?: string;
|
||||||
showArrow?: boolean;
|
showArrow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Premium Button: Pill-shaped, binary-accent hover effect, unified design.
|
||||||
|
*
|
||||||
|
* On hover:
|
||||||
|
* - A stream of binary characters scrolls across the button background
|
||||||
|
* - Primary: white binary on dark bg
|
||||||
|
* - Outline: blue binary on transparent bg
|
||||||
|
* - Subtle, fast, and satisfying
|
||||||
|
*/
|
||||||
export const Button: React.FC<ButtonProps> = ({
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
variant = "primary",
|
||||||
|
size = "normal",
|
||||||
className = "",
|
className = "",
|
||||||
showArrow = true
|
showArrow = true,
|
||||||
}) => {
|
}) => {
|
||||||
const baseStyles = "inline-flex items-center gap-4 rounded-full font-bold uppercase tracking-widest transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] group";
|
const [hovered, setHovered] = React.useState(false);
|
||||||
|
const [displayText, setDisplayText] = React.useState<string | null>(null);
|
||||||
const variants = {
|
const contentRef = React.useRef<HTMLSpanElement>(null);
|
||||||
primary: "px-10 py-5 bg-slate-900 text-white hover:bg-slate-800 hover:-translate-y-1 hover:shadow-2xl hover:shadow-slate-900/20 text-sm",
|
|
||||||
outline: "px-8 py-4 border border-slate-200 bg-white text-slate-900 hover:border-slate-400 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 text-sm"
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = (
|
// Binary scramble on hover
|
||||||
<>
|
React.useEffect(() => {
|
||||||
{children}
|
if (!hovered) {
|
||||||
{showArrow && <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />}
|
setDisplayText(null);
|
||||||
</>
|
return;
|
||||||
|
}
|
||||||
|
const original = contentRef.current?.textContent || "";
|
||||||
|
if (!original) return;
|
||||||
|
const chars = original.split("");
|
||||||
|
const total = 20;
|
||||||
|
let frame = 0;
|
||||||
|
const iv = setInterval(() => {
|
||||||
|
frame++;
|
||||||
|
const s = chars.map((c, i) => {
|
||||||
|
if (c === " ") return " ";
|
||||||
|
const settle = (frame / total) * chars.length;
|
||||||
|
if (i < settle) return c;
|
||||||
|
return Math.random() > 0.5 ? "1" : "0";
|
||||||
|
});
|
||||||
|
setDisplayText(s.join(""));
|
||||||
|
if (frame >= total) {
|
||||||
|
setDisplayText(null);
|
||||||
|
clearInterval(iv);
|
||||||
|
}
|
||||||
|
}, 1000 / 60);
|
||||||
|
return () => clearInterval(iv);
|
||||||
|
}, [hovered]);
|
||||||
|
|
||||||
|
const [binaryStr, setBinaryStr] = React.useState(
|
||||||
|
"0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (href.startsWith('#')) {
|
React.useEffect(() => {
|
||||||
|
setBinaryStr(
|
||||||
|
Array.from({ length: 60 }, () => (Math.random() > 0.5 ? "0" : "1")).join(
|
||||||
|
" ",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const base =
|
||||||
|
"relative inline-flex items-center justify-center gap-3 overflow-hidden rounded-full font-bold uppercase tracking-[0.15em] transition-all duration-300 group cursor-pointer";
|
||||||
|
|
||||||
|
const sizes: Record<string, string> = {
|
||||||
|
normal: "px-8 py-4 text-[10px]",
|
||||||
|
large: "px-10 py-5 text-[11px]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants: Record<string, string> = {
|
||||||
|
primary:
|
||||||
|
"bg-slate-900 text-white hover:shadow-xl hover:shadow-slate-900/20 hover:-translate-y-0.5",
|
||||||
|
outline:
|
||||||
|
"border border-slate-200 bg-transparent text-slate-900 hover:border-slate-400 hover:-translate-y-0.5",
|
||||||
|
ghost: "bg-transparent text-slate-500 hover:text-slate-900",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Binary stream overlay colors by variant
|
||||||
|
const binaryColor =
|
||||||
|
variant === "primary"
|
||||||
|
? "rgba(255,255,255,0.06)"
|
||||||
|
: variant === "outline"
|
||||||
|
? "rgba(59,130,246,0.08)"
|
||||||
|
: "rgba(148,163,184,0.06)";
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<motion.span
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
className={`${base} ${sizes[size]} ${variants[variant]} ${className}`}
|
||||||
|
>
|
||||||
|
{/* Binary stream hover overlay */}
|
||||||
|
<span
|
||||||
|
className="absolute inset-0 flex items-center pointer-events-none overflow-hidden"
|
||||||
|
style={{ opacity: hovered ? 1 : 0, transition: "opacity 0.3s ease" }}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className="whitespace-nowrap font-mono text-[8px] tracking-[0.3em] select-none"
|
||||||
|
style={{ color: binaryColor }}
|
||||||
|
animate={hovered ? { x: [0, -200] } : { x: 0 }}
|
||||||
|
transition={
|
||||||
|
hovered ? { duration: 3, repeat: Infinity, ease: "linear" } : {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{binaryStr} {binaryStr}
|
||||||
|
</motion.span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Shimmer line on hover (top edge) */}
|
||||||
|
<span
|
||||||
|
className="absolute top-0 left-0 right-0 h-px pointer-events-none"
|
||||||
|
style={{
|
||||||
|
opacity: hovered ? 1 : 0,
|
||||||
|
transition: "opacity 0.5s ease",
|
||||||
|
background:
|
||||||
|
variant === "primary"
|
||||||
|
? "linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)"
|
||||||
|
: "linear-gradient(90deg, transparent, rgba(59,130,246,0.15), transparent)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<span
|
||||||
|
className="relative z-10 flex items-center gap-3"
|
||||||
|
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||||
|
>
|
||||||
|
<span ref={contentRef}>{displayText ?? children}</span>
|
||||||
|
{showArrow && (
|
||||||
|
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-1 transition-transform duration-300" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href.startsWith("#")) {
|
||||||
return (
|
return (
|
||||||
<a href={href} className={`${baseStyles} ${variants[variant]} ${className}`}>
|
<a
|
||||||
{content}
|
href={href}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector(href)?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inner}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Link href={href}>{inner}</Link>;
|
||||||
<Link href={href} className={`${baseStyles} ${variants[variant]} ${className}`}>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MotionButton: React.FC<ButtonProps> = ({
|
|
||||||
href,
|
|
||||||
children,
|
|
||||||
variant = 'primary',
|
|
||||||
className = "",
|
|
||||||
showArrow = true
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<Button href={href} variant={variant} className={className} showArrow={showArrow}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
181
apps/web/src/components/Effects/AbstractCircuit.tsx
Normal file
181
apps/web/src/components/Effects/AbstractCircuit.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractCircuit: Premium Canvas Binary Data Flow
|
||||||
|
*
|
||||||
|
* - Binary 0/1 characters flow in structured horizontal lanes
|
||||||
|
* - Vertical cross-traffic for depth
|
||||||
|
* - Characters "breathe" with sine-wave opacity
|
||||||
|
* - High performance: uses requestAnimationFrame, minimal allocations per frame
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Char {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
val: number; // 0 or 1
|
||||||
|
speed: number; // px per frame
|
||||||
|
vertical: boolean;
|
||||||
|
size: number;
|
||||||
|
baseAlpha: number;
|
||||||
|
phase: number; // sine-wave phase offset
|
||||||
|
flipIn: number; // frames until next flip
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AbstractCircuit: React.FC<{
|
||||||
|
invert?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ invert = false, className = "" }) => {
|
||||||
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||||
|
const stateRef = React.useRef({
|
||||||
|
chars: [] as Char[],
|
||||||
|
frame: 0,
|
||||||
|
dpr: 1,
|
||||||
|
w: 0,
|
||||||
|
h: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d", { alpha: true })!;
|
||||||
|
const s = stateRef.current;
|
||||||
|
let raf = 0;
|
||||||
|
|
||||||
|
// ── Sizing ──
|
||||||
|
const resize = () => {
|
||||||
|
s.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
s.w = canvas.offsetWidth;
|
||||||
|
s.h = canvas.offsetHeight;
|
||||||
|
canvas.width = s.w * s.dpr;
|
||||||
|
canvas.height = s.h * s.dpr;
|
||||||
|
seed();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Seed characters ──
|
||||||
|
const LANE = 28;
|
||||||
|
const GAP = 20;
|
||||||
|
|
||||||
|
const seed = () => {
|
||||||
|
const chars: Char[] = [];
|
||||||
|
const { w, h } = s;
|
||||||
|
|
||||||
|
// Horizontal lanes
|
||||||
|
for (let y = 0; y < h; y += LANE) {
|
||||||
|
const dir = Math.floor(y / LANE) % 3 === 0 ? -1 : 1;
|
||||||
|
const spd = (0.15 + Math.random() * 0.6) * dir;
|
||||||
|
for (let x = 0; x < w + GAP; x += GAP) {
|
||||||
|
if (Math.random() > 0.45) continue;
|
||||||
|
chars.push({
|
||||||
|
x: x + (Math.random() - 0.5) * 6,
|
||||||
|
y: y + LANE / 2 + (Math.random() - 0.5) * 4,
|
||||||
|
val: Math.random() > 0.5 ? 1 : 0,
|
||||||
|
speed: spd,
|
||||||
|
vertical: false,
|
||||||
|
size: 9 + Math.floor(Math.random() * 2),
|
||||||
|
baseAlpha: 0.035 + Math.random() * 0.045,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
flipIn: 120 + Math.floor(Math.random() * 400),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical lanes (sparser)
|
||||||
|
for (let x = 0; x < w; x += LANE * 2.5) {
|
||||||
|
const dir = Math.floor(x / LANE) % 2 === 0 ? 1 : -1;
|
||||||
|
const spd = (0.1 + Math.random() * 0.4) * dir;
|
||||||
|
for (let y = 0; y < h + GAP; y += GAP) {
|
||||||
|
if (Math.random() > 0.3) continue;
|
||||||
|
chars.push({
|
||||||
|
x: x + (Math.random() - 0.5) * 4,
|
||||||
|
y: y + (Math.random() - 0.5) * 6,
|
||||||
|
val: Math.random() > 0.5 ? 1 : 0,
|
||||||
|
speed: spd,
|
||||||
|
vertical: true,
|
||||||
|
size: 8 + Math.floor(Math.random() * 2),
|
||||||
|
baseAlpha: 0.025 + Math.random() * 0.035,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
flipIn: 150 + Math.floor(Math.random() * 500),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.chars = chars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Mouse tracking — use canvas-relative coordinates ──
|
||||||
|
|
||||||
|
// ── Render loop ──
|
||||||
|
const render = () => {
|
||||||
|
const { w, h, dpr, chars } = s;
|
||||||
|
s.frame++;
|
||||||
|
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
const t = s.frame * 0.02; // global time
|
||||||
|
|
||||||
|
// ── Draw characters ──
|
||||||
|
for (let i = 0, len = chars.length; i < len; i++) {
|
||||||
|
const c = chars[i];
|
||||||
|
|
||||||
|
// Move
|
||||||
|
if (c.vertical) {
|
||||||
|
c.y += c.speed;
|
||||||
|
if (c.y > h + 15) c.y = -15;
|
||||||
|
else if (c.y < -15) c.y = h + 15;
|
||||||
|
} else {
|
||||||
|
c.x += c.speed;
|
||||||
|
if (c.x > w + 15) c.x = -15;
|
||||||
|
else if (c.x < -15) c.x = w + 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip timer
|
||||||
|
if (--c.flipIn <= 0) {
|
||||||
|
c.val ^= 1;
|
||||||
|
c.flipIn = 120 + Math.floor(Math.random() * 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sine-wave "breathing"
|
||||||
|
const breath = Math.sin(t + c.phase) * 0.015;
|
||||||
|
|
||||||
|
let alpha = c.baseAlpha + breath;
|
||||||
|
|
||||||
|
// ── Draw ──
|
||||||
|
const sz = c.size;
|
||||||
|
ctx.font = `bold ${sz}px "SF Mono", "Fira Code", "Cascadia Code", "Menlo", monospace`;
|
||||||
|
|
||||||
|
ctx.fillStyle = invert
|
||||||
|
? `rgba(255,255,255,${Math.max(alpha, 0)})`
|
||||||
|
: `rgba(100,116,139,${Math.max(alpha, 0)})`;
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
|
ctx.fillText(c.val ? "1" : "0", c.x, c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Event listeners ──
|
||||||
|
// Listen on the PARENT element (the section) to capture all mouse movement
|
||||||
|
window.addEventListener("resize", resize);
|
||||||
|
|
||||||
|
resize();
|
||||||
|
raf = requestAnimationFrame(render);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("resize", resize);
|
||||||
|
};
|
||||||
|
}, [invert]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 overflow-hidden ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
63
apps/web/src/components/Effects/BinaryStream.tsx
Normal file
63
apps/web/src/components/Effects/BinaryStream.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface BinaryStreamProps {
|
||||||
|
className?: string;
|
||||||
|
columns?: number;
|
||||||
|
side?: "left" | "right" | "both";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BinaryStream: React.FC<BinaryStreamProps> = ({
|
||||||
|
className = "",
|
||||||
|
columns = 4,
|
||||||
|
side = "both",
|
||||||
|
}) => {
|
||||||
|
// Generate deterministic binary strings
|
||||||
|
const generateColumn = (seed: number) => {
|
||||||
|
const chars: string[] = [];
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
chars.push(((seed * 137 + i * 31) % 2).toString());
|
||||||
|
}
|
||||||
|
return chars.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderColumns = (position: "left" | "right") => (
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 ${position === "left" ? "left-0" : "right-0"} flex gap-3 pointer-events-none select-none overflow-hidden`}
|
||||||
|
style={{ height: "100%", width: `${columns * 16}px` }}
|
||||||
|
>
|
||||||
|
{Array.from({ length: columns }).map((_, i) => {
|
||||||
|
const offset = position === "left" ? i : i + columns;
|
||||||
|
const duration = 20 + (i % 3) * 8;
|
||||||
|
const delay = i * 2.5;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${position}-${i}`}
|
||||||
|
className="binary-column text-[10px] font-mono leading-[1.6] whitespace-pre tracking-widest"
|
||||||
|
style={{
|
||||||
|
color: "rgba(203, 213, 225, 0.12)",
|
||||||
|
writingMode: "vertical-lr",
|
||||||
|
animation: `binary-scroll ${duration}s linear ${delay}s infinite`,
|
||||||
|
animationFillMode: "backwards",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{generateColumn(offset + 42)}
|
||||||
|
{"\n"}
|
||||||
|
{generateColumn(offset + 99)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{(side === "left" || side === "both") && renderColumns("left")}
|
||||||
|
{(side === "right" || side === "both") && renderColumns("right")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
317
apps/web/src/components/Effects/CircuitBoard.tsx
Normal file
317
apps/web/src/components/Effects/CircuitBoard.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
connections: number[];
|
||||||
|
pulsePhase: number;
|
||||||
|
pulseSpeed: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Trace {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
progress: number;
|
||||||
|
speed: number;
|
||||||
|
active: boolean;
|
||||||
|
delay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CircuitBoardProps {
|
||||||
|
className?: string;
|
||||||
|
density?: "low" | "medium" | "high";
|
||||||
|
animate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CircuitBoard: React.FC<CircuitBoardProps> = ({
|
||||||
|
className = "",
|
||||||
|
density = "medium",
|
||||||
|
animate = true,
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const nodesRef = useRef<Node[]>([]);
|
||||||
|
const tracesRef = useRef<Trace[]>([]);
|
||||||
|
const animationFrameRef = useRef<number>(0);
|
||||||
|
const timeRef = useRef<number>(0);
|
||||||
|
const dimensionsRef = useRef<{ width: number; height: number }>({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const densityMap = { low: 12, medium: 20, high: 30 };
|
||||||
|
|
||||||
|
const initCircuit = useCallback(
|
||||||
|
(width: number, height: number) => {
|
||||||
|
const nodeCount = densityMap[density];
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const traces: Trace[] = [];
|
||||||
|
const gridCols = Math.ceil(Math.sqrt(nodeCount * (width / height)));
|
||||||
|
const gridRows = Math.ceil(nodeCount / gridCols);
|
||||||
|
const cellW = width / gridCols;
|
||||||
|
const cellH = height / gridRows;
|
||||||
|
|
||||||
|
// Create nodes on a jittered grid
|
||||||
|
for (let row = 0; row < gridRows; row++) {
|
||||||
|
for (let col = 0; col < gridCols; col++) {
|
||||||
|
if (nodes.length >= nodeCount) break;
|
||||||
|
nodes.push({
|
||||||
|
x: cellW * (col + 0.3 + Math.random() * 0.4),
|
||||||
|
y: cellH * (row + 0.3 + Math.random() * 0.4),
|
||||||
|
connections: [],
|
||||||
|
pulsePhase: Math.random() * Math.PI * 2,
|
||||||
|
pulseSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
size: 1.5 + Math.random() * 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect nearby nodes with orthogonal traces (PCB style)
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const maxConnections = 2 + Math.floor(Math.random() * 2);
|
||||||
|
const distances: { idx: number; dist: number }[] = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < nodes.length; j++) {
|
||||||
|
if (i === j) continue;
|
||||||
|
const dx = nodes[j].x - nodes[i].x;
|
||||||
|
const dy = nodes[j].y - nodes[i].y;
|
||||||
|
distances.push({ idx: j, dist: Math.sqrt(dx * dx + dy * dy) });
|
||||||
|
}
|
||||||
|
|
||||||
|
distances.sort((a, b) => a.dist - b.dist);
|
||||||
|
let connected = 0;
|
||||||
|
|
||||||
|
for (const d of distances) {
|
||||||
|
if (connected >= maxConnections) break;
|
||||||
|
if (d.dist > Math.max(cellW, cellH) * 2) break;
|
||||||
|
|
||||||
|
// Avoid duplicate traces
|
||||||
|
const exists = traces.some(
|
||||||
|
(t) =>
|
||||||
|
(t.from === i && t.to === d.idx) ||
|
||||||
|
(t.from === d.idx && t.to === i),
|
||||||
|
);
|
||||||
|
if (exists) continue;
|
||||||
|
|
||||||
|
nodes[i].connections.push(d.idx);
|
||||||
|
nodes[d.idx].connections.push(i);
|
||||||
|
traces.push({
|
||||||
|
from: i,
|
||||||
|
to: d.idx,
|
||||||
|
progress: 0,
|
||||||
|
speed: 0.002 + Math.random() * 0.004,
|
||||||
|
active: Math.random() > 0.6,
|
||||||
|
delay: Math.random() * 3000,
|
||||||
|
});
|
||||||
|
connected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodesRef.current = nodes;
|
||||||
|
tracesRef.current = traces;
|
||||||
|
},
|
||||||
|
[density],
|
||||||
|
);
|
||||||
|
|
||||||
|
const drawTrace = useCallback(
|
||||||
|
(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x1: number,
|
||||||
|
y1: number,
|
||||||
|
x2: number,
|
||||||
|
y2: number,
|
||||||
|
alpha: number,
|
||||||
|
) => {
|
||||||
|
// Draw orthogonal PCB-style trace (L-shaped)
|
||||||
|
const midX = Math.random() > 0.5 ? x2 : x1;
|
||||||
|
const midY = Math.random() > 0.5 ? y1 : y2;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
|
||||||
|
ctx.lineTo(x2, y1);
|
||||||
|
ctx.lineTo(x2, y2);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x1, y2);
|
||||||
|
ctx.lineTo(x2, y2);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = `rgba(203, 213, 225, ${alpha})`;
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
ctx.stroke();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const animateFrame = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext("2d");
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
const { width, height } = dimensionsRef.current;
|
||||||
|
const nodes = nodesRef.current;
|
||||||
|
const traces = tracesRef.current;
|
||||||
|
timeRef.current += 16;
|
||||||
|
const time = timeRef.current;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw static traces
|
||||||
|
traces.forEach((trace) => {
|
||||||
|
const from = nodes[trace.from];
|
||||||
|
const to = nodes[trace.to];
|
||||||
|
if (!from || !to) return;
|
||||||
|
|
||||||
|
// Static trace line
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(from.x, from.y);
|
||||||
|
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) {
|
||||||
|
ctx.lineTo(to.x, from.y);
|
||||||
|
ctx.lineTo(to.x, to.y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(from.x, to.y);
|
||||||
|
ctx.lineTo(to.x, to.y);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = "rgba(226, 232, 240, 0.4)";
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Animated data packet traveling along trace
|
||||||
|
if (animate && trace.active && time > trace.delay) {
|
||||||
|
trace.progress += trace.speed;
|
||||||
|
if (trace.progress > 1) {
|
||||||
|
trace.progress = 0;
|
||||||
|
trace.active = Math.random() > 0.3;
|
||||||
|
trace.delay = time + Math.random() * 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = trace.progress;
|
||||||
|
let px: number, py: number;
|
||||||
|
|
||||||
|
if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) {
|
||||||
|
if (p < 0.5) {
|
||||||
|
px = from.x + (to.x - from.x) * (p * 2);
|
||||||
|
py = from.y;
|
||||||
|
} else {
|
||||||
|
px = to.x;
|
||||||
|
py = from.y + (to.y - from.y) * ((p - 0.5) * 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (p < 0.5) {
|
||||||
|
px = from.x;
|
||||||
|
py = from.y + (to.y - from.y) * (p * 2);
|
||||||
|
} else {
|
||||||
|
px = from.x + (to.x - from.x) * ((p - 0.5) * 2);
|
||||||
|
py = to.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data packet glow
|
||||||
|
const gradient = ctx.createRadialGradient(px, py, 0, px, py, 8);
|
||||||
|
gradient.addColorStop(0, "rgba(148, 163, 184, 0.6)");
|
||||||
|
gradient.addColorStop(1, "rgba(148, 163, 184, 0)");
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(px - 8, py - 8, 16, 16);
|
||||||
|
|
||||||
|
// Data packet dot
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(px, py, 1.5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "rgba(148, 163, 184, 0.8)";
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const pulse = animate
|
||||||
|
? 0.3 + Math.sin(time * 0.001 * node.pulseSpeed + node.pulsePhase) * 0.2
|
||||||
|
: 0.4;
|
||||||
|
|
||||||
|
// Node glow
|
||||||
|
const gradient = ctx.createRadialGradient(
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
0,
|
||||||
|
node.x,
|
||||||
|
node.y,
|
||||||
|
node.size * 4,
|
||||||
|
);
|
||||||
|
gradient.addColorStop(0, `rgba(191, 203, 219, ${pulse * 0.3})`);
|
||||||
|
gradient.addColorStop(1, "rgba(191, 203, 219, 0)");
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(
|
||||||
|
node.x - node.size * 4,
|
||||||
|
node.y - node.size * 4,
|
||||||
|
node.size * 8,
|
||||||
|
node.size * 8,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Node dot
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(203, 213, 225, ${pulse + 0.2})`;
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw subtle binary text near some nodes
|
||||||
|
if (animate) {
|
||||||
|
ctx.font = "9px ui-monospace, monospace";
|
||||||
|
nodes.forEach((node, i) => {
|
||||||
|
if (i % 4 !== 0) return; // Only every 4th node
|
||||||
|
const binaryAlpha =
|
||||||
|
0.06 + Math.sin(time * 0.0008 + node.pulsePhase) * 0.04;
|
||||||
|
ctx.fillStyle = `rgba(148, 163, 184, ${binaryAlpha})`;
|
||||||
|
const binary = ((time * 0.01 + i * 137) % 256)
|
||||||
|
.toString(2)
|
||||||
|
.padStart(8, "0");
|
||||||
|
ctx.fillText(binary, node.x + node.size * 3, node.y + 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameRef.current = requestAnimationFrame(animateFrame);
|
||||||
|
}, [animate]);
|
||||||
|
|
||||||
|
const handleResize = useCallback(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!container || !canvas) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
canvas.style.width = `${rect.width}px`;
|
||||||
|
canvas.style.height = `${rect.height}px`;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
dimensionsRef.current = { width: rect.width, height: rect.height };
|
||||||
|
initCircuit(rect.width, rect.height);
|
||||||
|
}, [initCircuit]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
animationFrameRef.current = requestAnimationFrame(animateFrame);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
};
|
||||||
|
}, [handleResize, animateFrame]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`absolute inset-0 pointer-events-none ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<canvas ref={canvasRef} className="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
195
apps/web/src/components/Effects/CodeSnippet.tsx
Normal file
195
apps/web/src/components/Effects/CodeSnippet.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { motion, useInView } from "framer-motion";
|
||||||
|
|
||||||
|
interface CodeSnippetProps {
|
||||||
|
className?: string;
|
||||||
|
variant?: "code" | "git" | "terminal";
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeLines = [
|
||||||
|
{ indent: 0, text: "async deploy(config) {", color: "text-slate-500" },
|
||||||
|
{ indent: 1, text: "const build = await compile({", color: "text-slate-400" },
|
||||||
|
{ indent: 2, text: 'target: "production",', color: "text-slate-300" },
|
||||||
|
{ indent: 2, text: "optimize: true,", color: "text-slate-300" },
|
||||||
|
{ indent: 2, text: 'performance: "maximum"', color: "text-slate-300" },
|
||||||
|
{ indent: 1, text: "});", color: "text-slate-400" },
|
||||||
|
{ indent: 1, text: "", color: "" },
|
||||||
|
{ indent: 1, text: "await pipeline.run([", color: "text-slate-400" },
|
||||||
|
{ indent: 2, text: "lint, test, build, stage", color: "text-slate-300" },
|
||||||
|
{ indent: 1, text: "]);", color: "text-slate-400" },
|
||||||
|
{ indent: 1, text: "", color: "" },
|
||||||
|
{ indent: 1, text: 'return { status: "live" };', color: "text-slate-400" },
|
||||||
|
{ indent: 0, text: "}", color: "text-slate-500" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const gitBranches = [
|
||||||
|
{
|
||||||
|
type: "commit",
|
||||||
|
branch: "main",
|
||||||
|
label: "v2.1.0 – Production",
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "branch",
|
||||||
|
branch: "feature",
|
||||||
|
label: "feature/redesign",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{ type: "commit", branch: "feature", label: "Neues Layout", active: true },
|
||||||
|
{
|
||||||
|
type: "commit",
|
||||||
|
branch: "feature",
|
||||||
|
label: "Performance-Optimierung",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{ type: "merge", branch: "main", label: "Merge → Production", active: false },
|
||||||
|
{ type: "commit", branch: "main", label: "v2.2.0 – Live", active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const terminalLines = [
|
||||||
|
{ prompt: true, text: "npm run build", delay: 0 },
|
||||||
|
{ prompt: false, text: "✓ Compiled successfully", delay: 0.3 },
|
||||||
|
{ prompt: false, text: "✓ Lighthouse: 98/100", delay: 0.6 },
|
||||||
|
{ prompt: false, text: "✓ Bundle: 42kb gzipped", delay: 0.9 },
|
||||||
|
{ prompt: true, text: "git push origin main", delay: 1.5 },
|
||||||
|
{ prompt: false, text: "→ Deploying to production...", delay: 1.8 },
|
||||||
|
{ prompt: false, text: "✓ Live in 12s", delay: 2.4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
import { CodeWindow } from "./CodeWindow";
|
||||||
|
|
||||||
|
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
|
||||||
|
className = "",
|
||||||
|
variant = "code",
|
||||||
|
}) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
||||||
|
const [visibleLineIndex, setVisibleLineIndex] = useState(-1);
|
||||||
|
const [displayText, setDisplayText] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInView) return;
|
||||||
|
|
||||||
|
const lines =
|
||||||
|
variant === "code"
|
||||||
|
? codeLines
|
||||||
|
: variant === "git"
|
||||||
|
? gitBranches.map((b) => ({ text: b.label }))
|
||||||
|
: terminalLines;
|
||||||
|
|
||||||
|
const animate = async () => {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
setVisibleLineIndex(i);
|
||||||
|
const line = lines[i];
|
||||||
|
const text = "text" in line ? line.text : (line as any).label || "";
|
||||||
|
|
||||||
|
for (let j = 0; j <= text.length; j++) {
|
||||||
|
setDisplayText((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
next[i] = text.slice(0, j);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
const speed = "prompt" in line && line.prompt ? 40 : 25;
|
||||||
|
await new Promise((r) => setTimeout(r, speed));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pause = "delay" in line ? (line as any).delay * 1000 : 150;
|
||||||
|
await new Promise((r) => setTimeout(r, pause));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}, [isInView, variant]);
|
||||||
|
|
||||||
|
const title =
|
||||||
|
variant === "code"
|
||||||
|
? "deploy.ts"
|
||||||
|
: variant === "git"
|
||||||
|
? "git log"
|
||||||
|
: "terminal";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeWindow title={title} className={className} minHeight="380px">
|
||||||
|
<div ref={ref}>
|
||||||
|
{variant === "code" &&
|
||||||
|
codeLines.map((line, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={i <= visibleLineIndex ? { opacity: 1 } : { opacity: 0 }}
|
||||||
|
className={`${line.color} whitespace-pre flex items-center h-6`}
|
||||||
|
style={{ paddingLeft: `${line.indent * 20}px` }}
|
||||||
|
>
|
||||||
|
<span>{displayText[i] || ""}</span>
|
||||||
|
{i === visibleLineIndex && (
|
||||||
|
<motion.span
|
||||||
|
animate={{ opacity: [1, 0] }}
|
||||||
|
transition={{ repeat: Infinity, duration: 0.6 }}
|
||||||
|
className="inline-block w-1.5 h-4 bg-slate-300 ml-1 shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{variant === "git" && (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute left-[7px] top-3 bottom-3 w-px bg-slate-200" />
|
||||||
|
{gitBranches.map((item, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, x: -5 }}
|
||||||
|
animate={i <= visibleLineIndex ? { opacity: 1, x: 0 } : {}}
|
||||||
|
className="flex items-center gap-4 py-1.5 relative h-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 z-10 shrink-0 ${item.type === "merge" ? "border-slate-400 bg-slate-100" : item.active ? "border-slate-300 bg-white" : "border-slate-200 bg-slate-50"}`}
|
||||||
|
/>
|
||||||
|
{item.type === "branch" && (
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 border border-slate-200 text-slate-400 font-bold shrink-0">
|
||||||
|
{item.branch}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-xs ${item.active ? "text-slate-500" : "text-slate-300"} truncate`}
|
||||||
|
>
|
||||||
|
{displayText[i] || ""}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === "terminal" &&
|
||||||
|
terminalLines.map((line, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={i <= visibleLineIndex ? { opacity: 1 } : {}}
|
||||||
|
className="flex items-start gap-2 py-0.5 min-h-[1.5rem]"
|
||||||
|
>
|
||||||
|
{line.prompt && (
|
||||||
|
<span className="text-slate-300 select-none shrink-0">❯</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
line.prompt ? "text-slate-500 font-medium" : "text-slate-300"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{displayText[i] || ""}
|
||||||
|
</span>
|
||||||
|
{i === visibleLineIndex && (
|
||||||
|
<motion.span
|
||||||
|
animate={{ opacity: [1, 0] }}
|
||||||
|
transition={{ repeat: Infinity, duration: 0.6 }}
|
||||||
|
className="inline-block w-1.5 h-4 bg-slate-300 ml-0.5 shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CodeWindow>
|
||||||
|
);
|
||||||
|
};
|
||||||
59
apps/web/src/components/Effects/CodeWindow.tsx
Normal file
59
apps/web/src/components/Effects/CodeWindow.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
|
||||||
|
interface CodeWindowProps {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
fixedHeight?: boolean;
|
||||||
|
minHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodeWindow: A shared, stable browser-frame chassis for code, terminal, and diagrams.
|
||||||
|
* - Enforces dimension stability to prevent layout shifts.
|
||||||
|
* - Standardizes the "Systems, not Brochures" aesthetic.
|
||||||
|
*/
|
||||||
|
export const CodeWindow: React.FC<CodeWindowProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
actions,
|
||||||
|
fixedHeight = false,
|
||||||
|
minHeight = "380px",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative rounded-xl border border-slate-100 bg-slate-50/50 backdrop-blur-sm overflow-hidden w-full max-w-[600px] mx-auto flex-shrink-0 flex flex-col",
|
||||||
|
fixedHeight && "h-[400px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={!fixedHeight && minHeight ? { minHeight } : {}}
|
||||||
|
>
|
||||||
|
{/* Window chrome */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-white/50 shrink-0">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-slate-200" />
|
||||||
|
<span className="ml-3 text-[9px] font-mono text-slate-300 uppercase tracking-widest select-none">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content area */}
|
||||||
|
<div className="flex-1 overflow-x-auto overflow-y-auto p-4 md:p-6 font-mono text-xs md:text-sm leading-relaxed relative">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom gradient fade for aesthetics/depth */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-slate-50/80 to-transparent pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
120
apps/web/src/components/Effects/DataFlow.tsx
Normal file
120
apps/web/src/components/Effects/DataFlow.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface DataFlowProps {
|
||||||
|
className?: string;
|
||||||
|
lines?: number;
|
||||||
|
speed?: "slow" | "normal" | "fast";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataFlow: React.FC<DataFlowProps> = ({
|
||||||
|
className = "",
|
||||||
|
lines = 3,
|
||||||
|
speed = "normal",
|
||||||
|
}) => {
|
||||||
|
const speedMap = { slow: "8s", normal: "5s", fast: "3s" };
|
||||||
|
const duration = speedMap[speed];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full overflow-hidden ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1200 40"
|
||||||
|
className="w-full h-8 md:h-10"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
{Array.from({ length: lines }).map((_, i) => {
|
||||||
|
const y = 8 + (i * 24) / lines;
|
||||||
|
const delay = i * 0.8;
|
||||||
|
return (
|
||||||
|
<g key={i}>
|
||||||
|
{/* Static trace line */}
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1={y}
|
||||||
|
x2="1200"
|
||||||
|
y2={y}
|
||||||
|
stroke="rgba(226, 232, 240, 0.3)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
/>
|
||||||
|
{/* Animated data packet */}
|
||||||
|
<circle r="2" fill="rgba(148, 163, 184, 0.6)">
|
||||||
|
<animateMotion
|
||||||
|
dur={duration}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${delay}s`}
|
||||||
|
path={`M-20,${y} L1220,${y}`}
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
{/* Trailing glow */}
|
||||||
|
<circle r="6" fill="rgba(148, 163, 184, 0.08)">
|
||||||
|
<animateMotion
|
||||||
|
dur={duration}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${delay}s`}
|
||||||
|
path={`M-20,${y} L1220,${y}`}
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
{/* Secondary packet (opposite direction, slower) */}
|
||||||
|
<rect
|
||||||
|
width="12"
|
||||||
|
height="1"
|
||||||
|
rx="0.5"
|
||||||
|
fill="rgba(203, 213, 225, 0.3)"
|
||||||
|
>
|
||||||
|
<animateMotion
|
||||||
|
dur={`${parseFloat(duration) * 1.4}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
begin={`${delay + 2}s`}
|
||||||
|
path={`M1220,${y + 2} L-20,${y + 2}`}
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Junction nodes */}
|
||||||
|
{[200, 500, 800, 1050].map((x, i) => (
|
||||||
|
<g key={`node-${i}`}>
|
||||||
|
<circle cx={x} cy="20" r="2" fill="rgba(203, 213, 225, 0.4)">
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
values="1.5;2.5;1.5"
|
||||||
|
dur="3s"
|
||||||
|
begin={`${i * 0.7}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
<circle
|
||||||
|
cx={x}
|
||||||
|
cy="20"
|
||||||
|
r="6"
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(203, 213, 225, 0.15)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="r"
|
||||||
|
values="4;8;4"
|
||||||
|
dur="3s"
|
||||||
|
begin={`${i * 0.7}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.3;0;0.3"
|
||||||
|
dur="3s"
|
||||||
|
begin={`${i * 0.7}s`}
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
82
apps/web/src/components/Effects/GradientMesh.tsx
Normal file
82
apps/web/src/components/Effects/GradientMesh.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface GradientMeshProps {
|
||||||
|
className?: string;
|
||||||
|
variant?: "subtle" | "metallic" | "warm";
|
||||||
|
animate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GradientMesh: React.FC<GradientMeshProps> = ({
|
||||||
|
className = "",
|
||||||
|
variant = "subtle",
|
||||||
|
animate = true,
|
||||||
|
}) => {
|
||||||
|
const gradients = {
|
||||||
|
subtle: {
|
||||||
|
bg: "transparent",
|
||||||
|
blob1: "rgba(226, 232, 240, 0.6)",
|
||||||
|
blob2: "rgba(241, 245, 249, 0.7)",
|
||||||
|
blob3: "rgba(203, 213, 225, 0.35)",
|
||||||
|
},
|
||||||
|
metallic: {
|
||||||
|
bg: "transparent",
|
||||||
|
blob1: "rgba(186, 206, 235, 0.4)",
|
||||||
|
blob2: "rgba(214, 224, 240, 0.5)",
|
||||||
|
blob3: "rgba(170, 190, 220, 0.25)",
|
||||||
|
},
|
||||||
|
warm: {
|
||||||
|
bg: "transparent",
|
||||||
|
blob1: "rgba(241, 245, 249, 0.6)",
|
||||||
|
blob2: "rgba(248, 250, 252, 0.7)",
|
||||||
|
blob3: "rgba(226, 232, 240, 0.4)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = gradients[variant];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 pointer-events-none overflow-hidden ${className}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{/* Large Blob 1 */}
|
||||||
|
<div
|
||||||
|
className={`absolute rounded-full ${animate ? "animate-gradient-blob-1" : ""}`}
|
||||||
|
style={{
|
||||||
|
width: "900px",
|
||||||
|
height: "900px",
|
||||||
|
background: `radial-gradient(circle, ${colors.blob1} 0%, transparent 65%)`,
|
||||||
|
top: "-20%",
|
||||||
|
left: "-10%",
|
||||||
|
filter: "blur(80px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Large Blob 2 */}
|
||||||
|
<div
|
||||||
|
className={`absolute rounded-full ${animate ? "animate-gradient-blob-2" : ""}`}
|
||||||
|
style={{
|
||||||
|
width: "800px",
|
||||||
|
height: "800px",
|
||||||
|
background: `radial-gradient(circle, ${colors.blob2} 0%, transparent 65%)`,
|
||||||
|
bottom: "-25%",
|
||||||
|
right: "-10%",
|
||||||
|
filter: "blur(70px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Accent Blob 3 */}
|
||||||
|
<div
|
||||||
|
className={`absolute rounded-full ${animate ? "animate-gradient-blob-3" : ""}`}
|
||||||
|
style={{
|
||||||
|
width: "600px",
|
||||||
|
height: "600px",
|
||||||
|
background: `radial-gradient(circle, ${colors.blob3} 0%, transparent 65%)`,
|
||||||
|
top: "30%",
|
||||||
|
left: "25%",
|
||||||
|
filter: "blur(60px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
6
apps/web/src/components/Effects/index.ts
Normal file
6
apps/web/src/components/Effects/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { CircuitBoard } from "./CircuitBoard";
|
||||||
|
export { DataFlow } from "./DataFlow";
|
||||||
|
export { BinaryStream } from "./BinaryStream";
|
||||||
|
export { GradientMesh } from "./GradientMesh";
|
||||||
|
export { CodeSnippet } from "./CodeSnippet";
|
||||||
|
export { AbstractCircuit } from "./AbstractCircuit";
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from "react";
|
||||||
import * as Prism from 'prismjs';
|
import * as Prism from "prismjs";
|
||||||
import 'prismjs/components/prism-python';
|
import "prismjs/components/prism-python";
|
||||||
import 'prismjs/components/prism-typescript';
|
import "prismjs/components/prism-typescript";
|
||||||
import 'prismjs/components/prism-javascript';
|
import "prismjs/components/prism-javascript";
|
||||||
import 'prismjs/components/prism-jsx';
|
import "prismjs/components/prism-jsx";
|
||||||
import 'prismjs/components/prism-tsx';
|
import "prismjs/components/prism-tsx";
|
||||||
import 'prismjs/components/prism-docker';
|
import "prismjs/components/prism-docker";
|
||||||
import 'prismjs/components/prism-yaml';
|
import "prismjs/components/prism-yaml";
|
||||||
import 'prismjs/components/prism-json';
|
import "prismjs/components/prism-json";
|
||||||
import 'prismjs/components/prism-markup';
|
import "prismjs/components/prism-markup";
|
||||||
import 'prismjs/components/prism-css';
|
import "prismjs/components/prism-css";
|
||||||
import 'prismjs/components/prism-sql';
|
import "prismjs/components/prism-sql";
|
||||||
import 'prismjs/components/prism-bash';
|
import "prismjs/components/prism-bash";
|
||||||
import 'prismjs/components/prism-markdown';
|
import "prismjs/components/prism-markdown";
|
||||||
|
|
||||||
|
import { cn } from "../utils/cn";
|
||||||
|
import { CodeWindow } from "./Effects/CodeWindow";
|
||||||
|
|
||||||
interface FileExampleProps {
|
interface FileExampleProps {
|
||||||
filename: string;
|
filename: string;
|
||||||
@@ -26,40 +29,34 @@ interface FileExampleProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prismLanguageMap: Record<string, string> = {
|
const prismLanguageMap: Record<string, string> = {
|
||||||
py: 'python',
|
py: "python",
|
||||||
ts: 'typescript',
|
ts: "typescript",
|
||||||
tsx: 'tsx',
|
tsx: "tsx",
|
||||||
js: 'javascript',
|
js: "javascript",
|
||||||
jsx: 'jsx',
|
jsx: "jsx",
|
||||||
dockerfile: 'docker',
|
dockerfile: "docker",
|
||||||
docker: 'docker',
|
docker: "docker",
|
||||||
yml: 'yaml',
|
yml: "yaml",
|
||||||
yaml: 'yaml',
|
yaml: "yaml",
|
||||||
json: 'json',
|
json: "json",
|
||||||
html: 'markup',
|
html: "markup",
|
||||||
css: 'css',
|
css: "css",
|
||||||
sql: 'sql',
|
sql: "sql",
|
||||||
sh: 'bash',
|
sh: "bash",
|
||||||
bash: 'bash',
|
bash: "bash",
|
||||||
md: 'markdown',
|
md: "markdown",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FileExample: React.FC<FileExampleProps> = ({
|
export const FileExample: React.FC<FileExampleProps> = ({
|
||||||
filename,
|
filename,
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
id
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const safeId = String(id).replace(/[^a-zA-Z0-9_-]/g, '');
|
const fileExtension = filename.split(".").pop() || language;
|
||||||
const headerId = `file-example-header-${safeId}`;
|
const prismLanguage = prismLanguageMap[fileExtension] || "markup";
|
||||||
const contentId = `file-example-content-${safeId}`;
|
|
||||||
|
|
||||||
const fileExtension = filename.split('.').pop() || language;
|
|
||||||
const prismLanguage = prismLanguageMap[fileExtension] || 'markup';
|
|
||||||
|
|
||||||
const highlightedCode = Prism.highlight(
|
const highlightedCode = Prism.highlight(
|
||||||
content,
|
content,
|
||||||
@@ -67,15 +64,6 @@ export const FileExample: React.FC<FileExampleProps> = ({
|
|||||||
prismLanguage,
|
prismLanguage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleExpand = () => {
|
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
if (!isExpanded) {
|
|
||||||
setTimeout(() => {
|
|
||||||
contentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
}, 120);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = async (e: React.MouseEvent) => {
|
const handleCopy = async (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
@@ -83,15 +71,15 @@ export const FileExample: React.FC<FileExampleProps> = ({
|
|||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
setTimeout(() => setIsCopied(false), 900);
|
setTimeout(() => setIsCopied(false), 900);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy:', err);
|
console.error("Failed to copy:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (e: React.MouseEvent) => {
|
const handleDownload = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const blob = new Blob([content], { type: 'text/plain' });
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
@@ -100,93 +88,93 @@ export const FileExample: React.FC<FileExampleProps> = ({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const actions = (
|
||||||
<div
|
<>
|
||||||
className="file-example w-full bg-white border border-slate-200 rounded-2xl overflow-hidden transition-all duration-300"
|
<button
|
||||||
data-file-example
|
type="button"
|
||||||
data-expanded={isExpanded}
|
className={`h-7 w-7 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 transition-all ${isCopied ? "text-green-500 border-green-200" : ""}`}
|
||||||
>
|
onClick={handleCopy}
|
||||||
<div
|
title="Copy to clipboard"
|
||||||
className="px-4 py-3 flex items-center justify-between gap-3 cursor-pointer select-none bg-white hover:bg-slate-50 transition-colors"
|
|
||||||
onClick={toggleExpand}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleExpand();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
aria-controls={contentId}
|
|
||||||
id={headerId}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<svg
|
||||||
<svg
|
className="w-3 h-3"
|
||||||
className={`w-3 h-3 text-slate-400 flex-shrink-0 transition-transform ${isExpanded ? 'rotate-0' : '-rotate-90'}`}
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span className="text-xs font-mono text-slate-900 truncate" title={filename}>{filename}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`copy-btn h-8 w-8 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100 ${isCopied ? 'copied' : ''}`}
|
|
||||||
onClick={handleCopy}
|
|
||||||
title="Copy to clipboard"
|
|
||||||
aria-label={`Copy ${filename} to clipboard`}
|
|
||||||
data-copied={isCopied}
|
|
||||||
>
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="download-btn h-8 w-8 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100"
|
|
||||||
onClick={handleDownload}
|
|
||||||
title="Download file"
|
|
||||||
aria-label={`Download ${filename}`}
|
|
||||||
>
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={contentRef}
|
|
||||||
className={`file-example__content overflow-hidden transition-[max-height,opacity] duration-200 ease-out bg-white ${isExpanded ? 'max-h-[22rem] opacity-100' : 'max-h-0 opacity-0'}`}
|
|
||||||
id={contentId}
|
|
||||||
role="region"
|
|
||||||
aria-labelledby={headerId}
|
|
||||||
>
|
|
||||||
<pre
|
|
||||||
className="m-0 p-6 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar border-t border-slate-200"
|
|
||||||
style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", maxHeight: "22rem" }}
|
|
||||||
>
|
>
|
||||||
<code className={`language-${prismLanguage}`} dangerouslySetInnerHTML={{ __html: highlightedCode }}></code>
|
<path
|
||||||
</pre>
|
strokeLinecap="round"
|
||||||
</div>
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-7 w-7 inline-flex items-center justify-center rounded-full border border-slate-200 bg-white text-slate-400 hover:text-slate-900 hover:border-slate-400 transition-all"
|
||||||
|
onClick={handleDownload}
|
||||||
|
title="Download file"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-3 h-3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-8 group/file">
|
||||||
|
<CodeWindow
|
||||||
|
title={filename}
|
||||||
|
actions={actions}
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300",
|
||||||
|
isExpanded
|
||||||
|
? "max-w-4xl"
|
||||||
|
: "max-w-[600px] cursor-pointer hover:border-slate-200",
|
||||||
|
)}
|
||||||
|
minHeight={isExpanded ? "auto" : "80px"}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
!isExpanded && "max-h-[80px] overflow-hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
className="m-0 p-0 overflow-x-auto text-[13px] leading-[1.65] font-mono text-slate-800"
|
||||||
|
style={{
|
||||||
|
fontFamily:
|
||||||
|
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<code
|
||||||
|
className={`language-${prismLanguage}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||||
|
></code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
{!isExpanded && (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-white/80 to-transparent flex items-end justify-center pb-2">
|
||||||
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||||
|
Click to expand
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CodeWindow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,61 @@
|
|||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
import LogoBlack from '../assets/logo/Logo Black Transparent.svg';
|
import LogoBlack from "../assets/logo/Logo Black Transparent.svg";
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="py-16 mt-24 border-t border-slate-100 bg-white relative z-10">
|
<footer className="relative py-16 mt-24 border-t border-slate-100 bg-white z-10">
|
||||||
|
{/* Tech border at top */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-px overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full w-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, transparent 0%, rgba(148, 163, 184, 0.15) 25%, rgba(191, 206, 228, 0.1) 50%, rgba(148, 163, 184, 0.15) 75%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="narrow-container">
|
<div className="narrow-container">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Image
|
<Image src={LogoBlack} alt="Marc Mintel" height={72} />
|
||||||
src={LogoBlack}
|
|
||||||
alt="Marc Mintel"
|
|
||||||
height={72}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:items-end gap-4 text-sm font-mono text-slate-300 uppercase tracking-widest">
|
<div className="flex flex-col md:items-end gap-4 text-sm font-mono text-slate-300 uppercase tracking-widest">
|
||||||
<span>© {currentYear}</span>
|
<span>© {currentYear}</span>
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<a href="/about" className="hover:text-slate-900 transition-colors no-underline">Über mich</a>
|
<a
|
||||||
<a href="/contact" className="hover:text-slate-900 transition-colors no-underline">Kontakt</a>
|
href="/about"
|
||||||
<a href="https://github.com/marcmintel" className="hover:text-slate-900 transition-colors no-underline">GitHub</a>
|
className="hover:text-slate-900 transition-colors duration-300 no-underline"
|
||||||
|
>
|
||||||
|
Über mich
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/contact"
|
||||||
|
className="hover:text-slate-900 transition-colors duration-300 no-underline"
|
||||||
|
>
|
||||||
|
Kontakt
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/marcmintel"
|
||||||
|
className="hover:text-slate-900 transition-colors duration-300 no-underline"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Subtle binary decoration */}
|
||||||
|
<span
|
||||||
|
className="text-[8px] text-slate-200 tracking-[0.5em] select-none font-mono"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
01001101 01001001 01001110
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
124
apps/web/src/components/GlitchText.tsx
Normal file
124
apps/web/src/components/GlitchText.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useInView } from "framer-motion";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlitchText: Binary scramble reveal effect
|
||||||
|
*
|
||||||
|
* Text characters scramble through random binary (0/1) before settling
|
||||||
|
* into the final text. Creates a "decoding" feel.
|
||||||
|
*
|
||||||
|
* Can also be triggered on hover for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface GlitchTextProps {
|
||||||
|
children: string;
|
||||||
|
className?: string;
|
||||||
|
delay?: number;
|
||||||
|
duration?: number;
|
||||||
|
trigger?: "inView" | "hover" | "mount";
|
||||||
|
as?: "span" | "div" | "h1" | "h2" | "p";
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHARS = "01";
|
||||||
|
|
||||||
|
export const GlitchText: React.FC<GlitchTextProps> = ({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
delay = 0,
|
||||||
|
duration = 0.8,
|
||||||
|
trigger = "inView",
|
||||||
|
as: Tag = "span",
|
||||||
|
}) => {
|
||||||
|
const text = children;
|
||||||
|
const [display, setDisplay] = React.useState(text);
|
||||||
|
const [isRevealed, setIsRevealed] = React.useState(false);
|
||||||
|
const [isHovering, setIsHovering] = React.useState(false);
|
||||||
|
const ref = React.useRef<any>(null);
|
||||||
|
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
||||||
|
|
||||||
|
// Initial scramble reveal (on mount or in-view)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isRevealed) return;
|
||||||
|
|
||||||
|
const shouldStart =
|
||||||
|
trigger === "mount" || (trigger === "inView" && isInView);
|
||||||
|
if (!shouldStart) return;
|
||||||
|
|
||||||
|
const chars = text.split("");
|
||||||
|
const totalFrames = Math.ceil(duration * 60); // ~60fps
|
||||||
|
const staggerPerChar = totalFrames / chars.length;
|
||||||
|
let frame = 0;
|
||||||
|
|
||||||
|
const delayMs = delay * 1000;
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
frame++;
|
||||||
|
const revealed = chars.map((char, i) => {
|
||||||
|
if (char === " ") return " ";
|
||||||
|
const charFrame = i * staggerPerChar;
|
||||||
|
if (frame > charFrame + staggerPerChar * 2) return char; // Settled
|
||||||
|
if (frame > charFrame) {
|
||||||
|
// Scrambling phase
|
||||||
|
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
|
}
|
||||||
|
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
|
});
|
||||||
|
setDisplay(revealed.join(""));
|
||||||
|
|
||||||
|
if (frame >= totalFrames + staggerPerChar * 2) {
|
||||||
|
setDisplay(text);
|
||||||
|
setIsRevealed(true);
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 1000 / 60);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, delayMs);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [isInView, trigger, text, delay, duration, isRevealed]);
|
||||||
|
|
||||||
|
// Hover re-scramble (for buttons)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isHovering || trigger !== "hover") return;
|
||||||
|
|
||||||
|
const chars = text.split("");
|
||||||
|
const totalFrames = 20; // Quick scramble
|
||||||
|
let frame = 0;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
frame++;
|
||||||
|
const scrambled = chars.map((char, i) => {
|
||||||
|
if (char === " ") return " ";
|
||||||
|
const settle = (frame / totalFrames) * chars.length;
|
||||||
|
if (i < settle) return char;
|
||||||
|
return CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
|
});
|
||||||
|
setDisplay(scrambled.join(""));
|
||||||
|
|
||||||
|
if (frame >= totalFrames) {
|
||||||
|
setDisplay(text);
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 1000 / 60);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isHovering, trigger, text]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={ref}
|
||||||
|
className={`${className}`}
|
||||||
|
onMouseEnter={trigger === "hover" ? () => setIsHovering(true) : undefined}
|
||||||
|
onMouseLeave={
|
||||||
|
trigger === "hover" ? () => setIsHovering(false) : undefined
|
||||||
|
}
|
||||||
|
style={{ fontVariantNumeric: "tabular-nums" }} // Prevent layout shift during scramble
|
||||||
|
>
|
||||||
|
{display}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,75 +1,98 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from "next/navigation";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
import IconWhite from '../assets/logo/Icon White Transparent.svg';
|
import IconWhite from "../assets/logo/Icon White Transparent.svg";
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
export const Header: React.FC = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [, setIsScrolled] = React.useState(false);
|
const [isScrolled, setIsScrolled] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setIsScrolled(window.scrollY > 20);
|
setIsScrolled(window.scrollY > 20);
|
||||||
};
|
};
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener("scroll", handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isActive = (path: string) => pathname === path;
|
const isActive = (path: string) => pathname === path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white/80 backdrop-blur-md sticky top-0 z-50 border-b border-slate-50">
|
<header
|
||||||
|
className={`sticky top-0 z-50 transition-all duration-500 ${
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Animated tech border at bottom */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-px overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full w-full"
|
||||||
|
style={{
|
||||||
|
background: isScrolled
|
||||||
|
? "linear-gradient(90deg, transparent 0%, rgba(148, 163, 184, 0.15) 30%, rgba(191, 206, 228, 0.1) 50%, rgba(148, 163, 184, 0.15) 70%, transparent 100%)"
|
||||||
|
: "transparent",
|
||||||
|
transition: "background 0.5s ease",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="narrow-container py-4 flex items-center justify-between">
|
<div className="narrow-container py-4 flex items-center justify-between">
|
||||||
<Link href="/" className="flex items-center gap-4 group">
|
<Link href="/" className="flex items-center gap-4 group">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-black rounded-xl flex items-center justify-center group-hover:scale-105 transition-all duration-500 shadow-sm shrink-0">
|
<div className="w-12 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
|
<Image
|
||||||
src={IconWhite}
|
src={IconWhite}
|
||||||
alt="Marc Mintel Icon"
|
alt="Marc Mintel Icon"
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
className="w-8 h-8"
|
className="w-8 h-8 relative z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="flex items-center gap-8">
|
<nav className="flex items-center gap-8">
|
||||||
<Link
|
{[
|
||||||
href="/about"
|
{ href: "/about", label: "Über mich" },
|
||||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/about') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
{ href: "/websites", label: "Websites" },
|
||||||
}`}
|
{ href: "/case-studies", label: "Case Studies", prefix: true },
|
||||||
>
|
{ href: "/blog", label: "Blog", prefix: true },
|
||||||
Über mich
|
].map((link) => {
|
||||||
</Link>
|
const active = link.prefix
|
||||||
<Link
|
? isActive(link.href) || pathname?.startsWith(`${link.href}/`)
|
||||||
href="/websites"
|
: isActive(link.href);
|
||||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/websites') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
|
||||||
}`}
|
return (
|
||||||
>
|
<Link
|
||||||
Websites
|
key={link.href}
|
||||||
</Link>
|
href={link.href}
|
||||||
<Link
|
className={`text-xs font-bold uppercase tracking-widest transition-colors duration-300 relative ${
|
||||||
href="/case-studies"
|
active
|
||||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/case-studies') || pathname?.startsWith('/case-studies/') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
? "text-slate-900"
|
||||||
}`}
|
: "text-slate-400 hover:text-slate-900"
|
||||||
>
|
}`}
|
||||||
Case Studies
|
>
|
||||||
</Link>
|
{active && (
|
||||||
<Link
|
<span className="absolute -bottom-1 left-0 right-0 flex justify-center">
|
||||||
href="/blog"
|
<span className="w-1 h-1 rounded-full bg-slate-900 animate-circuit-pulse" />
|
||||||
className={`text-xs font-bold uppercase tracking-widest transition-colors ${isActive('/blog') || pathname?.startsWith('/blog/') ? 'text-slate-900' : 'text-slate-400 hover:text-slate-900'
|
</span>
|
||||||
}`}
|
)}
|
||||||
>
|
{link.label}
|
||||||
Blog
|
</Link>
|
||||||
</Link>
|
);
|
||||||
|
})}
|
||||||
<Link
|
<Link
|
||||||
href="/contact"
|
href="/contact"
|
||||||
className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-900 border border-slate-200 px-5 py-2.5 rounded-full hover:border-slate-400 hover:bg-slate-50 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:shadow-slate-100"
|
className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-900 border border-slate-200 px-5 py-2.5 rounded-full 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)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Anfrage
|
Anfrage
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
129
apps/web/src/components/HeroSection.tsx
Normal file
129
apps/web/src/components/HeroSection.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { motion, useScroll, useTransform } from "framer-motion";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { AbstractCircuit } from "./Effects";
|
||||||
|
import { GlitchText } from "./GlitchText";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HeroSection: "Binary Architecture / The Blueprint"
|
||||||
|
*
|
||||||
|
* - **Concept**: The website as a technical blueprint being rendered in real-time.
|
||||||
|
* - **Typography**: Massive scale, mixing black Sans and technical Mono.
|
||||||
|
* - **Details**: Coordinate markers, blueprint lines, and binary data integration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const HeroSection: React.FC = () => {
|
||||||
|
const { scrollY } = useScroll();
|
||||||
|
const y = useTransform(scrollY, [0, 800], [0, 300]);
|
||||||
|
const opacity = useTransform(scrollY, [0, 600], [1, 0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative min-h-[100vh] flex items-center justify-center overflow-hidden bg-white">
|
||||||
|
{/* 1. The Binary Architecture (Background) */}
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<AbstractCircuit />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Content Layer */}
|
||||||
|
<div className="relative z-10 container mx-auto px-6 h-full flex flex-col justify-center items-center">
|
||||||
|
<motion.div
|
||||||
|
style={{ y, opacity }}
|
||||||
|
className="text-center relative max-w-[90vw]"
|
||||||
|
>
|
||||||
|
{/* Architectural Coordinate Labels */}
|
||||||
|
<div className="absolute -left-20 top-0 hidden xl:flex flex-col gap-8 opacity-20 pointer-events-none">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-4">
|
||||||
|
<span className="text-[10px] font-mono">0x00{i}A</span>
|
||||||
|
<div className="w-12 h-[1px] bg-slate-400" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badge */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 1, delay: 0.2 }}
|
||||||
|
className="mb-10 inline-flex items-center gap-4 px-6 py-2 border border-slate-100 bg-white/40 backdrop-blur-sm rounded-full"
|
||||||
|
>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
|
||||||
|
<div className="w-1 h-1 rounded-full bg-blue-300 animate-pulse delay-100" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono font-bold tracking-[0.4em] text-slate-500 uppercase">
|
||||||
|
Digital_Architect // v.2026
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Headline */}
|
||||||
|
<h1 className="text-7xl md:text-[11rem] font-black tracking-tighter leading-[0.8] text-slate-900 mb-12 uppercase">
|
||||||
|
<div className="block">
|
||||||
|
<GlitchText delay={0.5} duration={1.2}>
|
||||||
|
Websites
|
||||||
|
</GlitchText>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="block font-mono text-transparent bg-clip-text bg-gradient-to-r from-slate-400 via-slate-300 to-slate-400 font-light italic"
|
||||||
|
style={{ letterSpacing: "-0.02em" }}
|
||||||
|
>
|
||||||
|
<GlitchText delay={0.9} duration={1}>
|
||||||
|
ohne overhead.
|
||||||
|
</GlitchText>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Subtext */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, delay: 0.8 }}
|
||||||
|
className="flex flex-col items-center gap-12"
|
||||||
|
>
|
||||||
|
<p className="text-xl md:text-3xl text-slate-400 font-medium max-w-2xl leading-relaxed">
|
||||||
|
Ein Entwickler. Ein Ansprechpartner. <br />
|
||||||
|
<span className="text-slate-900 font-bold tracking-tight">
|
||||||
|
Systematische Architekturen für das Web.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-6">
|
||||||
|
<Button href="/contact" size="large">
|
||||||
|
Projekt anfragen
|
||||||
|
</Button>
|
||||||
|
<Button href="/websites" variant="outline" size="large">
|
||||||
|
Prozess ansehen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. Blueprint Frame (Decorative Borders) */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none border-[1px] border-slate-100 m-8 opacity-40" />
|
||||||
|
<div className="absolute top-8 left-8 p-4 hidden md:block opacity-20 transform -rotate-90 origin-top-left transition-opacity hover:opacity-100 group">
|
||||||
|
<span className="text-[10px] font-mono tracking-widest text-slate-400">
|
||||||
|
POS_TRANSMISSION_001
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-8 right-8 p-4 hidden md:block opacity-20 transition-opacity hover:opacity-100">
|
||||||
|
<span className="text-[10px] font-mono tracking-widest text-slate-400">
|
||||||
|
EST_2026 // M-ARCH
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4. Scroll Indicator */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-3 opacity-40"
|
||||||
|
style={{ opacity }}
|
||||||
|
>
|
||||||
|
<div className="w-[1px] h-12 bg-slate-200" />
|
||||||
|
<span className="text-[9px] font-mono uppercase tracking-[0.3em] text-slate-400">
|
||||||
|
Scroll
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -33,10 +33,12 @@ export const IconListItem: React.FC<IconListItemProps> = ({
|
|||||||
let renderIcon = icon;
|
let renderIcon = icon;
|
||||||
|
|
||||||
if (bullet) {
|
if (bullet) {
|
||||||
renderIcon = <div className="w-1.5 h-1.5 bg-slate-900 rounded-full" />;
|
renderIcon = (
|
||||||
|
<div className="w-2 h-2 bg-slate-900 rounded-full shrink-0 group-hover:bg-blue-500 transition-colors duration-300" />
|
||||||
|
);
|
||||||
} else if (check) {
|
} else if (check) {
|
||||||
renderIcon = (
|
renderIcon = (
|
||||||
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform">
|
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center shrink-0 group-hover:scale-110 group-hover:shadow-lg group-hover:shadow-blue-500/10 transition-all duration-300">
|
||||||
<Check className="w-4 h-4 text-white" />
|
<Check className="w-4 h-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "../utils/cn";
|
import { cn } from "../utils/cn";
|
||||||
import { ShieldCheck } from "lucide-react";
|
import { ShieldCheck, ArrowLeft, ArrowRight, RefreshCw } from "lucide-react";
|
||||||
import { MonoLabel } from "./Typography";
|
import { MonoLabel, Label } from "./Typography";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
interface IframeSectionProps {
|
interface IframeSectionProps {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -24,42 +25,90 @@ interface IframeSectionProps {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
noScale?: boolean;
|
noScale?: boolean;
|
||||||
dynamicGlow?: boolean;
|
dynamicGlow?: boolean;
|
||||||
|
minHeight?: number;
|
||||||
|
mobileWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable Browser UI components to maintain consistency
|
* Reusable Browser UI components to maintain consistency
|
||||||
*/
|
*/
|
||||||
const BrowserChrome: React.FC<{ url: string; minimal?: boolean }> = ({
|
const BrowserChromeComponent: React.FC<{ url: string; minimal?: boolean }> = ({
|
||||||
url,
|
url,
|
||||||
minimal,
|
minimal,
|
||||||
}) => {
|
}) => {
|
||||||
if (minimal) return null;
|
if (minimal) return null;
|
||||||
return (
|
return (
|
||||||
<div className="h-14 bg-white/90 backdrop-blur-2xl border-b border-slate-200/40 flex items-center px-6 gap-8 z-[100] flex-shrink-0 relative">
|
<div
|
||||||
{/* Status Indicators (Traffic Lights) */}
|
className="h-12 md:h-14 bg-slate-50 border-b border-slate-200/60 flex items-center px-4 md:px-6 gap-4 md:gap-8 z-[100] flex-shrink-0 relative isolation-auto"
|
||||||
<div className="flex gap-1.5 opacity-40">
|
style={{ isolation: "isolate", minHeight: "48px" }}
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-slate-900" />
|
>
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-slate-900" />
|
{/* 3D Rim Highlight */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-[1px] bg-white opacity-80" />
|
||||||
|
|
||||||
|
{/* Status Indicators (Traffic Lights) - Enhanced with subtle depth */}
|
||||||
|
<div className="flex gap-1.5 md:gap-2.5">
|
||||||
|
{[
|
||||||
|
"bg-slate-300 from-slate-200 to-slate-400",
|
||||||
|
"bg-slate-300 from-slate-200 to-slate-400",
|
||||||
|
"bg-slate-300 from-slate-200 to-slate-400",
|
||||||
|
].map((cls, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
"w-2.5 h-2.5 md:w-3 md:h-3 rounded-full bg-gradient-to-br shadow-[inset_0_1px_2px_rgba(0,0,0,0.1),0_1.5px_1px_rgba(255,255,255,0.7)] relative flex items-center justify-center",
|
||||||
|
cls,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Subtle glint, soft blur */}
|
||||||
|
<div className="absolute top-[20%] left-[20%] w-[30%] h-[30%] rounded-full bg-white/30 blur-[0.7px]" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL Bar */}
|
{/* Navigation Controls - Hidden on mobile */}
|
||||||
<div className="flex-1 max-w-[600px] mx-auto bg-white/30 backdrop-blur-3xl rounded-full flex items-center justify-center px-6 h-8 border border-white/60 shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)]">
|
<div className="hidden lg:flex items-center gap-4 opacity-30">
|
||||||
<div className="flex items-center gap-3 opacity-80 group-hover:opacity-100 transition-all duration-700">
|
<ArrowLeft className="w-3.5 h-3.5" />
|
||||||
<ShieldCheck className="w-3.5 h-3.5 text-slate-900" />
|
<ArrowRight className="w-3.5 h-3.5" />
|
||||||
<span className="text-[10px] font-mono font-bold tracking-[0.25em] uppercase truncate whitespace-nowrap text-slate-900">
|
<RefreshCw className="w-3 h-3 ml-1" />
|
||||||
{url}
|
</div>
|
||||||
|
|
||||||
|
{/* URL Bar - Solid high-fidelity layer instead of expensive backdrop-blur */}
|
||||||
|
<div className="flex-1 max-w-[700px] mx-auto bg-white shadow-[inset_0_1px_3px_rgba(0,0,0,0.06),0_0_0_1px_rgba(255,255,255,0.4)] rounded-xl flex items-center justify-between px-3 md:px-6 h-8 md:h-9 border border-slate-200/40 group">
|
||||||
|
<div className="flex items-center gap-2 md:gap-3 opacity-60 group-hover:opacity-100 transition-opacity duration-700 overflow-hidden">
|
||||||
|
<ShieldCheck className="w-3 md:w-3.5 h-3 md:h-3.5 text-blue-500 shrink-0" />
|
||||||
|
<span className="text-[9px] md:text-[10px] font-mono font-bold tracking-[0.1em] md:tracking-[0.2em] uppercase truncate whitespace-nowrap text-slate-900 md:border-r border-slate-200 md:pr-4 md:mr-1">
|
||||||
|
{url.replace("varnish-cache://", "")}
|
||||||
|
</span>
|
||||||
|
<span className="text-[8px] font-mono text-slate-400 tracking-widest hidden xl:block">
|
||||||
|
VARNISH_TUNNEL // ALPHA_NODE_04
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse hidden sm:block shadow-[0_0_8px_rgba(74,222,128,0.4)]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Industrial Accent */}
|
{/* Industrial Accent / Technical ID - Hidden on mobile */}
|
||||||
<div className="flex items-center gap-2 opacity-30">
|
<div className="hidden md:flex flex-col items-end opacity-20 pointer-events-none select-none">
|
||||||
<div className="w-8 h-1 bg-slate-400 rounded-full" />
|
<div className="text-[6px] font-mono font-bold tracking-widest uppercase mb-0.5">
|
||||||
|
SECURE_LINK // ID: VARNISH_4X
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-0.5 bg-slate-900 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BrowserChromeComponent.propTypes = {
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
minimal: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BrowserChrome = React.memo(BrowserChromeComponent);
|
||||||
|
|
||||||
|
BrowserChrome.displayName = "BrowserChrome";
|
||||||
|
|
||||||
export const IframeSection: React.FC<IframeSectionProps> = ({
|
export const IframeSection: React.FC<IframeSectionProps> = ({
|
||||||
src,
|
src,
|
||||||
title,
|
title,
|
||||||
@@ -78,12 +127,18 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
delay = 0,
|
delay = 0,
|
||||||
noScale = false,
|
noScale = false,
|
||||||
dynamicGlow = true,
|
dynamicGlow = true,
|
||||||
|
minHeight = 400,
|
||||||
|
mobileWidth = 390,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||||
const [scale, setScale] = React.useState(1);
|
const [scale, setScale] = React.useState(1);
|
||||||
|
const [activeInternalWidth, setActiveInternalWidth] =
|
||||||
|
React.useState(desktopWidth);
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
const [isIframeLoaded, setIsIframeLoaded] = React.useState(false);
|
||||||
|
const [isMinTimePassed, setIsMinTimePassed] = React.useState(false);
|
||||||
const [glowColors, setGlowColors] = React.useState<string[]>([
|
const [glowColors, setGlowColors] = React.useState<string[]>([
|
||||||
"rgba(148, 163, 184, 0.1)",
|
"rgba(148, 163, 184, 0.1)",
|
||||||
"rgba(148, 163, 184, 0.1)",
|
"rgba(148, 163, 184, 0.1)",
|
||||||
@@ -97,28 +152,52 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scaling Logic
|
const [headerHeightPx, setHeaderHeightPx] = React.useState(0);
|
||||||
|
|
||||||
|
// Responsive Header Height Calculation
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (browserFrame && !minimal) {
|
||||||
|
const updateHeaderHeight = () => {
|
||||||
|
setHeaderHeightPx(window.innerWidth < 768 ? 48 : 56);
|
||||||
|
};
|
||||||
|
updateHeaderHeight();
|
||||||
|
window.addEventListener("resize", updateHeaderHeight);
|
||||||
|
return () => window.removeEventListener("resize", updateHeaderHeight);
|
||||||
|
} else {
|
||||||
|
setHeaderHeightPx(0);
|
||||||
|
}
|
||||||
|
}, [browserFrame, minimal]);
|
||||||
|
|
||||||
|
// Scaling & Adaptive Viewport Logic
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!containerRef.current || noScale) {
|
if (!containerRef.current || noScale) {
|
||||||
setScale(1);
|
setScale(1);
|
||||||
|
setActiveInternalWidth(desktopWidth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScale = () => {
|
const updateDimensions = () => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
const currentWidth = containerRef.current.offsetWidth;
|
const containerWidth = containerRef.current.offsetWidth;
|
||||||
if (currentWidth > 0) {
|
if (containerWidth > 0) {
|
||||||
const newScale = zoom || currentWidth / desktopWidth;
|
// Adaptive threshold: Switch to mobile width if container is small
|
||||||
|
const useMobile = containerWidth < 500;
|
||||||
|
const internalWidth = useMobile ? mobileWidth : desktopWidth;
|
||||||
|
|
||||||
|
setActiveInternalWidth(internalWidth);
|
||||||
|
|
||||||
|
// Calculate scale based on the active target width
|
||||||
|
const newScale = zoom || containerWidth / internalWidth;
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateScale();
|
updateDimensions();
|
||||||
const observer = new ResizeObserver(updateScale);
|
const observer = new ResizeObserver(updateDimensions);
|
||||||
observer.observe(containerRef.current);
|
observer.observe(containerRef.current);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [desktopWidth, zoom, noScale]);
|
}, [desktopWidth, mobileWidth, zoom, noScale]);
|
||||||
|
|
||||||
const updateScrollState = React.useCallback(() => {
|
const updateScrollState = React.useCallback(() => {
|
||||||
try {
|
try {
|
||||||
@@ -178,8 +257,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, [dynamicGlow, offsetY, updateScrollState]);
|
}, [dynamicGlow, offsetY, updateScrollState]);
|
||||||
|
|
||||||
const headerHeightPx = browserFrame && !minimal ? 56 : 0;
|
|
||||||
|
|
||||||
// Height parse helper
|
// Height parse helper
|
||||||
const parseNumericHeight = (h: string | number) => {
|
const parseNumericHeight = (h: string | number) => {
|
||||||
if (typeof h === "number") return h;
|
if (typeof h === "number") return h;
|
||||||
@@ -201,8 +278,57 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
: finalScaledHeight
|
: finalScaledHeight
|
||||||
? `${finalScaledHeight + headerHeightPx}px`
|
? `${finalScaledHeight + headerHeightPx}px`
|
||||||
: `calc(${height} + ${headerHeightPx}px)`,
|
: `calc(${height} + ${headerHeightPx}px)`,
|
||||||
|
minHeight: minHeight ? `${minHeight}px` : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [loadingPhase, setLoadingPhase] = React.useState(0);
|
||||||
|
const loadingPhases = [
|
||||||
|
"DETERMINING ROUTE",
|
||||||
|
"INITIALIZING HANDSHAKE",
|
||||||
|
"ESTABLISHING ENCRYPTED LINK",
|
||||||
|
"SYNCING ASSETS",
|
||||||
|
"FINALIZING...",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1. Safety Trigger: Force-stop loading after 2.5s no matter what
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLoading) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 2500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
// 2. Sync Trigger: Cleanup when BOTH phases and iframe load complete
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLoading) return;
|
||||||
|
|
||||||
|
// Early exit: if iframe is already loaded, we only need the first 2 "handshake" phases
|
||||||
|
const phasesRequired = isIframeLoaded
|
||||||
|
? loadingPhases.length - 2
|
||||||
|
: loadingPhases.length - 1;
|
||||||
|
|
||||||
|
if (loadingPhase >= phasesRequired) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isLoading, isIframeLoaded, loadingPhase, loadingPhases.length]);
|
||||||
|
|
||||||
|
// 3. Phase Incrementer (Faster: 400ms)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLoading) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setLoadingPhase((prev) => {
|
||||||
|
if (prev < loadingPhases.length - 1) return prev + 1;
|
||||||
|
clearInterval(interval);
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, 400);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isLoading, loadingPhases.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -231,11 +357,12 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full relative transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] flex flex-col z-10",
|
"w-full relative flex flex-col z-10",
|
||||||
minimal ? "bg-transparent" : "bg-slate-50",
|
minimal ? "bg-transparent" : "bg-slate-50",
|
||||||
!minimal &&
|
!minimal &&
|
||||||
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
"rounded-[2.5rem] border border-slate-200/50 shadow-[0_80px_160px_-40px_rgba(0,0,0,0.18),0_0_1px_rgba(0,0,0,0.1)]",
|
||||||
perspective && "hover:scale-[1.03] hover:-translate-y-3",
|
perspective &&
|
||||||
|
"hover:scale-[1.03] hover:-translate-y-3 transition-[transform,shadow,scale] duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)]",
|
||||||
"overflow-hidden",
|
"overflow-hidden",
|
||||||
)}
|
)}
|
||||||
style={chassisStyle}
|
style={chassisStyle}
|
||||||
@@ -266,94 +393,130 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scaled Viewport Container */}
|
{/* Scaled Viewport Container - Solid bg prevents gray flicker */}
|
||||||
<div className="flex-1 relative overflow-hidden bg-slate-50/50">
|
<div className="flex-1 relative overflow-hidden bg-white">
|
||||||
{/* Loader Overlay - Now scoped to viewport */}
|
{/* Artificial Loader Overlay */}
|
||||||
{isLoading && (
|
<AnimatePresence>
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-white/60 backdrop-blur-xl z-50 transition-opacity duration-700">
|
{isLoading && (
|
||||||
<div className="flex flex-col items-center gap-4">
|
<motion.div
|
||||||
<div className="w-12 h-12 border-[3px] border-slate-100 border-t-slate-900 rounded-full animate-spin" />
|
initial={{ opacity: 1 }}
|
||||||
<MonoLabel className="text-[10px] text-slate-400 animate-pulse uppercase tracking-[0.2em]">
|
exit={{
|
||||||
Establishing Connection
|
opacity: 0,
|
||||||
</MonoLabel>
|
transition: { duration: 0.6, ease: [0.23, 1, 0.32, 1] },
|
||||||
</div>
|
}}
|
||||||
</div>
|
className="absolute inset-0 flex items-center justify-center bg-white/95 backdrop-blur-3xl z-50 pointer-events-none transform-gpu"
|
||||||
)}
|
>
|
||||||
|
{/* Static Binary Matrix - Stop redundant re-paints */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[8px] p-4 flex flex-col gap-1 overflow-hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{Array.from({ length: 24 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="whitespace-nowrap tracking-[0.5em] opacity-50"
|
||||||
|
>
|
||||||
|
{"01011010010110101101011010101011010101011010101101010101101010"
|
||||||
|
.split("")
|
||||||
|
.join(" ")}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-6 relative z-10 w-full max-w-xs px-8">
|
||||||
|
<div className="w-full h-1 bg-slate-100 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{
|
||||||
|
width: `${((loadingPhase + 1) / loadingPhases.length) * 100}%`,
|
||||||
|
}}
|
||||||
|
className="h-full bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<MonoLabel className="text-[10px] text-slate-900 font-bold uppercase tracking-[0.3em] flex items-center gap-2">
|
||||||
|
<span className="w-1 h-1 rounded-full bg-blue-500 animate-pulse" />
|
||||||
|
{loadingPhases[loadingPhase]}
|
||||||
|
</MonoLabel>
|
||||||
|
<Label className="text-[8px] text-slate-400 font-mono uppercase tracking-widest">
|
||||||
|
Varnish_Tunnel // Alpha_Node
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0",
|
"transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] origin-top-left absolute left-0 right-0 transform-gpu",
|
||||||
noScale && "relative w-full h-full",
|
noScale && "relative w-full h-full",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: noScale ? "100%" : `${desktopWidth}px`,
|
width: noScale ? "100%" : `${activeInternalWidth}px`,
|
||||||
transform: noScale ? "none" : `scale(${scale})`,
|
transform: noScale ? "none" : `scale(${scale})`,
|
||||||
height: noScale ? "100%" : `${100 / scale}%`,
|
height: noScale ? "100%" : `${100 / scale}%`,
|
||||||
|
willChange: "opacity",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<iframe
|
<motion.div
|
||||||
ref={iframeRef}
|
initial={false}
|
||||||
src={src}
|
animate={{
|
||||||
scrolling={allowScroll ? "yes" : "no"}
|
opacity: isLoading ? 0 : 1,
|
||||||
className={cn(
|
}}
|
||||||
"w-full border-none transition-all duration-700 no-scrollbar relative z-0",
|
transition={{
|
||||||
isLoading ? "opacity-0 scale-95" : "opacity-100 scale-100",
|
duration: 0.6,
|
||||||
)}
|
ease: [0.23, 1, 0.32, 1],
|
||||||
onLoad={(e) => {
|
}}
|
||||||
setIsLoading(false);
|
className="w-full h-full relative z-0"
|
||||||
try {
|
>
|
||||||
const iframe = e.currentTarget;
|
<iframe
|
||||||
if (iframe.contentDocument) {
|
ref={iframeRef}
|
||||||
const style = iframe.contentDocument.createElement("style");
|
src={src}
|
||||||
style.textContent = `
|
scrolling={allowScroll ? "yes" : "no"}
|
||||||
*::-webkit-scrollbar { display: none !important; }
|
className="w-full h-full border-none no-scrollbar"
|
||||||
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
onLoad={(e) => {
|
||||||
body { background: transparent !important; }
|
setIsIframeLoaded(true);
|
||||||
`;
|
try {
|
||||||
iframe.contentDocument.head.appendChild(style);
|
const iframe = e.currentTarget;
|
||||||
setTimeout(updateAmbilight, 600);
|
if (iframe.contentDocument) {
|
||||||
|
const style =
|
||||||
|
iframe.contentDocument.createElement("style");
|
||||||
|
style.textContent = `
|
||||||
|
*::-webkit-scrollbar { display: none !important; }
|
||||||
|
* { -ms-overflow-style: none !important; scrollbar-width: none !important; }
|
||||||
|
body { background: transparent !important; }
|
||||||
|
`;
|
||||||
|
iframe.contentDocument.head.appendChild(style);
|
||||||
|
setTimeout(updateAmbilight, 600);
|
||||||
|
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
requestAnimationFrame(updateAmbilight);
|
requestAnimationFrame(updateAmbilight);
|
||||||
updateScrollState();
|
updateScrollState();
|
||||||
};
|
};
|
||||||
|
|
||||||
iframe.contentWindow?.addEventListener("scroll", onScroll, {
|
iframe.contentWindow?.addEventListener(
|
||||||
passive: true,
|
"scroll",
|
||||||
});
|
onScroll,
|
||||||
|
{
|
||||||
|
passive: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
iframe.contentWindow?.addEventListener(
|
style={{
|
||||||
"wheel",
|
transform: `translateY(-${offsetY}px)`,
|
||||||
(e) => {
|
height: `calc(100% + ${offsetY}px)`,
|
||||||
const { deltaY } = e as WheelEvent;
|
pointerEvents: allowScroll ? "auto" : "none",
|
||||||
const doc = iframe.contentDocument?.documentElement;
|
width: "calc(100% + 20px)", // Bleed for seamless edge
|
||||||
if (!doc) return;
|
marginLeft: "-10px",
|
||||||
const scrollTop = doc.scrollTop;
|
}}
|
||||||
const isAtTop = scrollTop <= 0;
|
title={title || "Project Display"}
|
||||||
const isAtBottom =
|
/>
|
||||||
scrollTop + doc.clientHeight >= doc.scrollHeight - 1;
|
</motion.div>
|
||||||
if (
|
|
||||||
(isAtTop && deltaY < 0) ||
|
|
||||||
(isAtBottom && deltaY > 0)
|
|
||||||
) {
|
|
||||||
window.scrollBy({ top: deltaY, behavior: "auto" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ passive: true },
|
|
||||||
);
|
|
||||||
} catch (_e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
transform: `translateY(-${offsetY}px)`,
|
|
||||||
height: `calc(100% + ${offsetY}px)`,
|
|
||||||
pointerEvents: allowScroll ? "auto" : "none",
|
|
||||||
width: "calc(100% + 20px)", // Bleed for seamless edge
|
|
||||||
marginLeft: "-10px",
|
|
||||||
}}
|
|
||||||
title={title || "Project Display"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Custom Industrial Scroll Indicator */}
|
{/* Custom Industrial Scroll Indicator */}
|
||||||
@@ -380,10 +543,6 @@ export const IframeSection: React.FC<IframeSectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!allowScroll && (
|
|
||||||
<div className="absolute inset-x-0 bottom-0 top-14 pointer-events-auto cursor-default z-20" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,63 +1,105 @@
|
|||||||
import * as React from 'react';
|
"use client";
|
||||||
import { cn } from '../utils/cn';
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../utils/cn";
|
||||||
|
|
||||||
export const BackgroundGrid: React.FC = () => (
|
export const BackgroundGrid: React.FC = () => (
|
||||||
<div className="fixed inset-0 pointer-events-none -z-20 opacity-[0.01]" style={{
|
<div className="fixed inset-0 pointer-events-none -z-20" aria-hidden="true">
|
||||||
backgroundImage: 'linear-gradient(#0f172a 1px, transparent 1px), linear-gradient(90deg, #0f172a 1px, transparent 1px)',
|
<div
|
||||||
backgroundSize: '60px 60px'
|
className="absolute inset-0 opacity-[0.015]"
|
||||||
}} />
|
style={{
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(#94a3b8 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, #94a3b8 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: "80px 80px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'white' | 'dark' | 'gray';
|
variant?: "white" | "dark" | "gray" | "glass";
|
||||||
hover?: boolean;
|
hover?: boolean;
|
||||||
padding?: 'none' | 'small' | 'normal' | 'large';
|
padding?: "none" | "small" | "normal" | "large";
|
||||||
|
techBorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Card: React.FC<CardProps> = ({
|
export const Card: React.FC<CardProps> = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
variant = 'white',
|
variant = "white",
|
||||||
hover = true,
|
hover = true,
|
||||||
padding = 'normal'
|
padding = "normal",
|
||||||
|
techBorder = false,
|
||||||
}) => {
|
}) => {
|
||||||
const variants = {
|
const variants = {
|
||||||
white: 'bg-white border-slate-100 text-slate-900 shadow-sm',
|
white: "bg-white border-slate-100 text-slate-900 shadow-sm",
|
||||||
dark: 'bg-slate-900 border-white/5 text-white shadow-xl',
|
dark: "bg-slate-900 border-white/5 text-white shadow-xl",
|
||||||
gray: 'bg-slate-50/50 border-slate-100 text-slate-900'
|
gray: "bg-slate-50/50 border-slate-100 text-slate-900",
|
||||||
|
glass: "glass text-slate-900",
|
||||||
};
|
};
|
||||||
|
|
||||||
const paddings = {
|
const paddings = {
|
||||||
none: 'p-0',
|
none: "p-0",
|
||||||
small: 'p-6 md:p-8',
|
small: "p-6 md:p-8",
|
||||||
normal: 'p-8 md:p-10',
|
normal: "p-8 md:p-10",
|
||||||
large: 'p-10 md:p-12'
|
large: "p-10 md:p-12",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [hexId, setHexId] = React.useState("0x0000");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setHexId(
|
||||||
|
`0x${Math.floor(Math.random() * 0xffff)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(4, "0")
|
||||||
|
.toUpperCase()}`,
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div
|
||||||
"rounded-2xl border h-full flex flex-col justify-between transition-all duration-500 ease-out",
|
className={cn(
|
||||||
variants[variant],
|
"rounded-2xl border h-full flex flex-col justify-between transition-all duration-500 ease-out relative overflow-hidden group/card",
|
||||||
paddings[padding],
|
variants[variant],
|
||||||
hover ? 'hover:border-slate-200 hover:shadow-md' : '',
|
paddings[padding],
|
||||||
className
|
hover ? "hover:border-slate-200 hover:shadow-md" : "",
|
||||||
)}>
|
techBorder ? "tech-border" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Binary corner accent on hover */}
|
||||||
|
{hover && (
|
||||||
|
<span
|
||||||
|
className="absolute top-3 right-3 text-[7px] font-mono tracking-widest opacity-0 group-hover/card:opacity-100 transition-opacity duration-500 select-none pointer-events-none"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
variant === "dark"
|
||||||
|
? "rgba(255,255,255,0.1)"
|
||||||
|
: "rgba(148,163,184,0.2)",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{hexId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Container: React.FC<{ children: React.ReactNode; className?: string; variant?: 'narrow' | 'normal' | 'wide' }> = ({
|
export const Container: React.FC<{
|
||||||
children,
|
children: React.ReactNode;
|
||||||
className = "",
|
className?: string;
|
||||||
variant = 'normal'
|
variant?: "narrow" | "normal" | "wide";
|
||||||
}) => {
|
}> = ({ children, className = "", variant = "normal" }) => {
|
||||||
const variants = {
|
const variants = {
|
||||||
narrow: 'max-w-4xl',
|
narrow: "max-w-4xl",
|
||||||
normal: 'max-w-6xl',
|
normal: "max-w-6xl",
|
||||||
wide: 'max-w-7xl'
|
wide: "max-w-7xl",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const Marker: React.FC<MarkerProps> = ({
|
|||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{children}
|
<span className="relative z-10 text-slate-900">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import mermaid from 'mermaid';
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
interface MermaidProps {
|
interface MermaidProps {
|
||||||
graph: string;
|
graph: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { CodeWindow } from "./Effects/CodeWindow";
|
||||||
|
|
||||||
export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
||||||
const [id, setId] = useState<string | null>(null);
|
const [id, setId] = useState<string | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setId(providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`);
|
setId(
|
||||||
|
providedId || `mermaid-${Math.random().toString(36).substring(2, 11)}`,
|
||||||
|
);
|
||||||
}, [providedId]);
|
}, [providedId]);
|
||||||
const [isRendered, setIsRendered] = useState(false);
|
const [isRendered, setIsRendered] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -21,17 +25,17 @@ export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: 'default',
|
theme: "default",
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
themeVariables: {
|
themeVariables: {
|
||||||
fontFamily: 'Inter, system-ui, sans-serif',
|
fontFamily: "Inter, system-ui, sans-serif",
|
||||||
fontSize: '16px',
|
fontSize: "16px",
|
||||||
primaryColor: '#ffffff',
|
primaryColor: "#ffffff",
|
||||||
nodeBorder: '#e2e8f0',
|
nodeBorder: "#e2e8f0",
|
||||||
mainBkg: '#ffffff',
|
mainBkg: "#ffffff",
|
||||||
lineColor: '#cbd5e1',
|
lineColor: "#cbd5e1",
|
||||||
},
|
},
|
||||||
securityLevel: 'loose',
|
securityLevel: "loose",
|
||||||
});
|
});
|
||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
@@ -41,8 +45,8 @@ export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
|||||||
containerRef.current.innerHTML = svg;
|
containerRef.current.innerHTML = svg;
|
||||||
setIsRendered(true);
|
setIsRendered(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Mermaid rendering failed:', err);
|
console.error("Mermaid rendering failed:", err);
|
||||||
setError('Failed to render diagram. Please check the syntax.');
|
setError("Failed to render diagram. Please check the syntax.");
|
||||||
setIsRendered(true);
|
setIsRendered(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,19 +61,11 @@ export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mermaid-wrapper my-8 not-prose">
|
<div className="mermaid-wrapper my-8 not-prose">
|
||||||
<div className="bg-white border border-slate-200 rounded-2xl overflow-hidden transition-all duration-500 hover:border-slate-400">
|
<CodeWindow title="Diagram" className="max-w-4xl" minHeight="300px">
|
||||||
<div className="px-4 py-3 border-b border-slate-100 bg-white flex items-center justify-between">
|
<div className="flex justify-center bg-white p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
<svg className="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em]">Diagram</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mermaid-container p-8 md:p-12 overflow-x-auto flex justify-center bg-white">
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`mermaid transition-opacity duration-500 w-full max-w-4xl ${isRendered ? 'opacity-100' : 'opacity-0'}`}
|
className={`mermaid transition-opacity duration-500 w-full max-w-4xl ${isRendered ? "opacity-100" : "opacity-0"}`}
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -81,7 +77,7 @@ export const Mermaid: React.FC<MermaidProps> = ({ graph, id: providedId }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CodeWindow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const Reveal: React.FC<RevealProps> = ({
|
|||||||
delay = 0.25,
|
delay = 0.25,
|
||||||
className = "",
|
className = "",
|
||||||
direction = "up",
|
direction = "up",
|
||||||
scale = 1,
|
scale = 0.98,
|
||||||
blur = false,
|
blur = true,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
const isInView = useInView(ref, { once: true, margin: "-10%" });
|
||||||
@@ -35,15 +35,17 @@ export const Reveal: React.FC<RevealProps> = ({
|
|||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
y: direction === "up" ? 10 : direction === "down" ? -10 : 0,
|
y: direction === "up" ? 15 : direction === "down" ? -15 : 0,
|
||||||
x: direction === "left" ? 10 : direction === "right" ? -10 : 0,
|
x: direction === "left" ? 15 : direction === "right" ? -15 : 0,
|
||||||
scale: scale !== 1 ? scale : 1,
|
scale: scale !== 1 ? scale : 1,
|
||||||
|
filter: blur ? "blur(4px)" : "none",
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
x: 0,
|
x: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
filter: "none",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Reveal } from './Reveal';
|
import { Reveal } from "./Reveal";
|
||||||
import { Label } from './Typography';
|
import { Label } from "./Typography";
|
||||||
import { cn } from '../utils/cn';
|
import { cn } from "../utils/cn";
|
||||||
|
import { GlitchText } from "./GlitchText";
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
number?: string;
|
number?: string;
|
||||||
@@ -9,11 +10,12 @@ interface SectionProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
variant?: 'white' | 'gray';
|
variant?: "white" | "gray" | "glass";
|
||||||
borderTop?: boolean;
|
borderTop?: boolean;
|
||||||
borderBottom?: boolean;
|
borderBottom?: boolean;
|
||||||
containerVariant?: 'narrow' | 'normal' | 'wide';
|
containerVariant?: "narrow" | "normal" | "wide";
|
||||||
illustration?: React.ReactNode;
|
illustration?: React.ReactNode;
|
||||||
|
effects?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Section: React.FC<SectionProps> = ({
|
export const Section: React.FC<SectionProps> = ({
|
||||||
@@ -22,25 +24,62 @@ export const Section: React.FC<SectionProps> = ({
|
|||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
delay = 0,
|
delay = 0,
|
||||||
variant = 'white',
|
variant = "white",
|
||||||
borderTop = false,
|
borderTop = false,
|
||||||
borderBottom = false,
|
borderBottom = false,
|
||||||
containerVariant = 'narrow',
|
containerVariant = "narrow",
|
||||||
illustration,
|
illustration,
|
||||||
|
effects,
|
||||||
}) => {
|
}) => {
|
||||||
const bgClass = variant === 'gray' ? 'bg-slate-50/50' : 'bg-white';
|
const bgClass = {
|
||||||
const borderTopClass = borderTop ? 'border-t border-slate-100' : '';
|
white: "bg-white",
|
||||||
const borderBottomClass = borderBottom ? 'border-b border-slate-100' : '';
|
gray: "bg-slate-50/50",
|
||||||
const containerClass = containerVariant === 'wide' ? 'wide-container' : containerVariant === 'normal' ? 'container' : 'narrow-container';
|
glass: "glass-subtle",
|
||||||
|
}[variant];
|
||||||
|
|
||||||
|
const borderTopClass = borderTop ? "border-t border-slate-100" : "";
|
||||||
|
const borderBottomClass = borderBottom ? "border-b border-slate-100" : "";
|
||||||
|
const containerClass =
|
||||||
|
containerVariant === "wide"
|
||||||
|
? "wide-container"
|
||||||
|
: containerVariant === "normal"
|
||||||
|
? "container"
|
||||||
|
: "narrow-container";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={cn(
|
<section
|
||||||
"relative py-24 md:py-40 group overflow-hidden",
|
className={cn(
|
||||||
bgClass,
|
"relative py-24 md:py-40 group overflow-hidden",
|
||||||
borderTopClass,
|
bgClass,
|
||||||
borderBottomClass,
|
borderTopClass,
|
||||||
className
|
borderBottomClass,
|
||||||
)}>
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Tech divider with binary accent */}
|
||||||
|
{borderTop && (
|
||||||
|
<div className="absolute top-0 left-0 right-0 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-px w-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, transparent 0%, rgba(148, 163, 184, 0.2) 20%, rgba(191, 206, 228, 0.15) 50%, rgba(148, 163, 184, 0.2) 80%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-center mt-2">
|
||||||
|
<span
|
||||||
|
className="text-[7px] font-mono tracking-[0.5em] text-slate-200 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
01001101 01001001 01001110
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Optional effects layer */}
|
||||||
|
{effects}
|
||||||
|
|
||||||
<div className={cn("relative z-10", containerClass)}>
|
<div className={cn("relative z-10", containerClass)}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24">
|
||||||
{/* Sidebar: Number & Title */}
|
{/* Sidebar: Number & Title */}
|
||||||
@@ -48,14 +87,25 @@ export const Section: React.FC<SectionProps> = ({
|
|||||||
<div className="md:sticky md:top-40 space-y-8">
|
<div className="md:sticky md:top-40 space-y-8">
|
||||||
{number && (
|
{number && (
|
||||||
<Reveal delay={delay}>
|
<Reveal delay={delay}>
|
||||||
<span className="block text-7xl md:text-8xl font-bold text-slate-100 leading-none select-none tracking-tighter">
|
<span className="block text-7xl md:text-8xl font-bold text-slate-100 leading-none select-none tracking-tighter relative">
|
||||||
{number}
|
{number}
|
||||||
|
{/* Subtle binary overlay on number */}
|
||||||
|
<span
|
||||||
|
className="absolute top-1 left-0 text-[8px] font-mono text-slate-200/30 tracking-wider leading-none select-none pointer-events-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{parseInt(number || "0")
|
||||||
|
.toString(2)
|
||||||
|
.padStart(8, "0")}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
{title && (
|
{title && (
|
||||||
<Reveal delay={delay + 0.1}>
|
<Reveal delay={delay + 0.1}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Animated dot indicator */}
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-slate-300 animate-circuit-pulse shrink-0" />
|
||||||
<Label className="text-slate-900 text-[10px] tracking-[0.4em]">
|
<Label className="text-slate-900 text-[10px] tracking-[0.4em]">
|
||||||
{title}
|
{title}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -73,9 +123,7 @@ export const Section: React.FC<SectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="md:col-span-9">
|
<div className="md:col-span-9">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./directus/uploads:/directus/uploads
|
- ./directus/uploads:/directus/uploads
|
||||||
- ./directus/extensions:/directus/extensions
|
- ./directus/extensions:/directus/extensions
|
||||||
- ./directus/schema:/directus/schema
|
|
||||||
- ./directus/migrations:/directus/migrations
|
- ./directus/migrations:/directus/migrations
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|||||||
@@ -103,20 +103,6 @@ services:
|
|||||||
- ./directus/extensions:/directus/extensions
|
- ./directus/extensions:/directus/extensions
|
||||||
- ./directus/schema:/directus/schema
|
- ./directus/schema:/directus/schema
|
||||||
- ./directus/migrations:/directus/migrations
|
- ./directus/migrations:/directus/migrations
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--no-verbose",
|
|
||||||
"--tries=1",
|
|
||||||
"--spider",
|
|
||||||
"http://127.0.0.1:8055/server/ping",
|
|
||||||
]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
- 'traefik.http.routers.${PROJECT_NAME:-mintel-me}-directus.rule=${TRAEFIK_DIRECTUS_RULE:-Host("${DIRECTUS_HOST:-cms.mintel.localhost}")}'
|
||||||
|
|||||||
Reference in New Issue
Block a user