From c1295546a6636ad0de14ade6f6b0a87be8b2336b Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 15 Feb 2026 17:34:07 +0100 Subject: [PATCH] feat: unify code-like components with shared CodeWindow, fix blog re-render loop, and stabilize layouts --- apps/web/app/about/page.tsx | 267 +++++------- apps/web/app/blog/page.tsx | 126 ++++-- apps/web/app/case-studies/klz-cables/page.tsx | 154 +++++-- apps/web/app/case-studies/page.tsx | 219 ++++++---- apps/web/app/contact/page.tsx | 101 +++-- apps/web/app/globals.css | 374 ++++++++++++++-- apps/web/app/page.tsx | 403 ++++++++---------- apps/web/app/websites/page.tsx | 322 +++++++------- apps/web/next.config.mjs | 9 + apps/web/src/components/Button.tsx | 189 ++++++-- .../components/Effects/AbstractCircuit.tsx | 181 ++++++++ .../src/components/Effects/BinaryStream.tsx | 63 +++ .../src/components/Effects/CircuitBoard.tsx | 317 ++++++++++++++ .../src/components/Effects/CodeSnippet.tsx | 195 +++++++++ .../web/src/components/Effects/CodeWindow.tsx | 59 +++ apps/web/src/components/Effects/DataFlow.tsx | 120 ++++++ .../src/components/Effects/GradientMesh.tsx | 82 ++++ apps/web/src/components/Effects/index.ts | 6 + apps/web/src/components/FileExample.tsx | 262 ++++++------ apps/web/src/components/Footer.tsx | 55 ++- apps/web/src/components/GlitchText.tsx | 124 ++++++ apps/web/src/components/Header.tsx | 105 +++-- apps/web/src/components/HeroSection.tsx | 129 ++++++ apps/web/src/components/IconList.tsx | 6 +- apps/web/src/components/IframeSection.tsx | 377 +++++++++++----- apps/web/src/components/Layout.tsx | 112 +++-- apps/web/src/components/Marker.tsx | 2 +- apps/web/src/components/Mermaid.tsx | 50 +-- apps/web/src/components/Reveal.tsx | 10 +- apps/web/src/components/Section.tsx | 94 +++- docker-compose.dev.yml | 1 - docker-compose.yml | 14 - 32 files changed, 3293 insertions(+), 1235 deletions(-) create mode 100644 apps/web/src/components/Effects/AbstractCircuit.tsx create mode 100644 apps/web/src/components/Effects/BinaryStream.tsx create mode 100644 apps/web/src/components/Effects/CircuitBoard.tsx create mode 100644 apps/web/src/components/Effects/CodeSnippet.tsx create mode 100644 apps/web/src/components/Effects/CodeWindow.tsx create mode 100644 apps/web/src/components/Effects/DataFlow.tsx create mode 100644 apps/web/src/components/Effects/GradientMesh.tsx create mode 100644 apps/web/src/components/Effects/index.ts create mode 100644 apps/web/src/components/GlitchText.tsx create mode 100644 apps/web/src/components/HeroSection.tsx diff --git a/apps/web/app/about/page.tsx b/apps/web/app/about/page.tsx index a10fabb..80bf8a9 100644 --- a/apps/web/app/about/page.tsx +++ b/apps/web/app/about/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import Image from "next/image"; import { PageHeader } from "../../src/components/PageHeader"; import { Section } from "../../src/components/Section"; @@ -6,7 +8,6 @@ import { ExperienceIllustration, ResponsibilityIllustration, ResultIllustration, - ConceptSystem, ContactIllustration, HeroLines, ParticleNetwork, @@ -21,26 +22,23 @@ import { Label, MonoLabel, } 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 { IconList, IconListItem } from "../../src/components/IconList"; +import { + GradientMesh, + CodeSnippet, + AbstractCircuit, +} from "../../src/components/Effects"; +import { Marker } from "../../src/components/Marker"; export default function AboutPage() { return (
- {/* Background Elements */} - - + {/* Hero Section */}
-
- -
-
- -
-
@@ -75,10 +73,11 @@ export default function AboutPage() { - Über mich. + Über mich. } - 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" />
@@ -89,7 +88,7 @@ export default function AboutPage() {
- {/* Section 01: Experience */} + {/* Section 01: Story */}

- 15 Jahre Web-Entwicklung.
- - Vom Designer zum Architekten. - + Vom Designer
+ zum Architekten.

@@ -110,17 +107,20 @@ export default function AboutPage() {
- Ich habe Agenturen, Konzerne und Startups von innen gesehen. - Dabei habe ich gelernt, what really counts:{" "} + Agenturen, Konzerne, Startups – ich habe die Branche von allen + Seiten kennengelernt. Was hängen geblieben ist:{" "} - Ergebnisse, nicht Prozesse. + + Ergebnisse zählen. + {" "} + Nicht der Weg dorthin. {[ - "Komplexe Systeme vereinfacht", - "Performance-Probleme gelöst", - "Nachhaltige Software-Architekturen gebaut", + "Frontend, Backend, Infrastruktur – Fullstack", + "Komplexe Systeme auf das Wesentliche reduziert", + "Performance-Probleme systematisch gelöst", ].map((item, i) => ( {item} @@ -137,8 +137,7 @@ export default function AboutPage() { className="group" >

- Mein Fokus heute: Direkte Zusammenarbeit ohne - Reibungsverluste. + Heute: Direkte Zusammenarbeit ohne Reibungsverluste.

{["Effizient", "Pragmatisch", "Verlässlich"].map((tag, i) => ( @@ -156,64 +155,77 @@ export default function AboutPage() {
- {/* Section 02: Responsibility */} + {/* Section 02: Arbeitsweise – HOW I work */}
} + effects={} >

- Ich stehe für meine
- Arbeit gerade. + So läuft ein Projekt
+ bei mir ab.

-
-
- - - In der klassischen Agenturwelt verschwindet Verantwortung oft - hinter Hierarchien. Bei mir gibt es nur{" "} - einen Ansprechpartner:{" "} - Mich. - - - - -
- ! + {/* Timeline Steps */} +
+ {/* Connecting line */} +
+ + {[ + { + step: "01", + title: "Briefing", + desc: "Sie beschreiben Ihr Vorhaben. Ich höre zu und stelle die richtigen Fragen.", + }, + { + step: "02", + title: "Angebot", + desc: "Ein Fixpreis-Angebot mit klarem Leistungsumfang. Keine Überraschungen.", + }, + { + 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) => ( + +
+
+
+ + {item.step} + +
- - Ich übernehme die volle Verantwortung für die technische - Umsetzung und Qualität Ihres Projekts. Ohne Ausreden. - - +
+

{item.title}

+ {item.desc} +
+
-
+ ))}
- {/* Section 03: Systems */} -
} - > + {/* Section 03: Philosophie – what drives me */} +

- Nachhaltigkeit durch
- sauberen Code. + Ich stehe für
+ meine Arbeit gerade.

@@ -221,24 +233,24 @@ export default function AboutPage() {
- Ich baue keine Wegwerf-Produkte. Meine Systeme sind so - konzipiert, dass sie mit Ihrem Unternehmen{" "} - wachsen können. + Keine Hierarchien, keine Ausreden. Wenn etwas nicht passt, + liegt die Verantwortung bei mir – und ich{" "} + + löse es. +
{[ - "Skalierbar", - "Wartbar", - "Performant", - "Sicher", - "Unabhängig", - "Zukunftssicher", + "Vollständige Transparenz", + "Ein Ansprechpartner", + "Messbare Qualität", + "Langfristige Partnerschaft", ].map((item, i) => (
-
- +
+
@@ -246,126 +258,47 @@ export default function AboutPage() { ))}
+ + {/* Decorative terminal */} - -
-

- Kein Vendor Lock-in. -

- - Sie behalten die volle Kontrolle über Ihren Code und Ihre - Daten. Keine Abhängigkeit von proprietären Systemen. - - +
- {/* Section 04: Result */} + {/* Section 04: CTA */}
} - > -
- -

- Was Sie von mir
- erwarten können. -

-
- -
-
-
- -
- {[ - "Agentur-Zirkus", - "Meeting-Marathon", - "Ticket-Wahnsinn", - "CMS-Frust", - ].map((item, i) => ( - - - {item} - - - ))} -
-
-
-
-
- - - {[ - { - 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) => ( - -
-

{item.label}

- - {item.desc} - -
-
- ))} -
-
-
-
-
-
- - {/* Section 05: Today */} -
} + effects={} >

Bereit für eine
- Zusammenarbeit? + Zusammenarbeit?

-
-
Lassen Sie uns gemeinsam etwas bauen, das{" "} - wirklich funktioniert. + + + wirklich funktioniert. + +
diff --git a/apps/web/app/blog/page.tsx b/apps/web/app/blog/page.tsx index 9c34824..fe58b3c 100644 --- a/apps/web/app/blog/page.tsx +++ b/apps/web/app/blog/page.tsx @@ -1,77 +1,113 @@ -'use client'; +"use client"; -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { MediumCard } from '../../src/components/MediumCard'; -import { SearchBar } from '../../src/components/SearchBar'; -import { Tag } from '../../src/components/Tag'; -import { blogPosts } from '../../src/data/blogPosts'; -import { PageHeader } from '../../src/components/PageHeader'; -import { Reveal } from '../../src/components/Reveal'; +import * as React from "react"; +import { useState, useEffect } from "react"; +import { MediumCard } from "../../src/components/MediumCard"; +import { SearchBar } from "../../src/components/SearchBar"; +import { Tag } from "../../src/components/Tag"; +import { blogPosts } from "../../src/data/blogPosts"; +import { PageHeader } from "../../src/components/PageHeader"; +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() { - const [searchQuery, setSearchQuery] = useState(''); - const [filteredPosts, setFilteredPosts] = useState(blogPosts); + const [searchQuery, setSearchQuery] = useState(""); - // Sort posts by date - const allPosts = [...blogPosts].sort((a, b) => - new Date(b.date).getTime() - new Date(a.date).getTime() + // Memoize allPosts to prevent infinite re-render loop + const allPosts = React.useMemo( + () => + [...blogPosts].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ), + [], ); - // Get unique tags - const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || []))); + const [filteredPosts, setFilteredPosts] = useState(allPosts); + + // Memoize allTags + const allTags = React.useMemo( + () => Array.from(new Set(allPosts.flatMap((post) => post.tags || []))), + [allPosts], + ); useEffect(() => { const query = searchQuery.toLowerCase().trim(); - if (query.startsWith('#')) { + if (query.startsWith("#")) { const tag = query.slice(1); - setFilteredPosts(allPosts.filter(post => - post.tags?.some(t => t.toLowerCase() === tag.toLowerCase()) - )); + setFilteredPosts( + allPosts.filter((post) => + post.tags?.some((t) => t.toLowerCase() === tag.toLowerCase()), + ), + ); } else { - setFilteredPosts(allPosts.filter(post => { - const title = post.title.toLowerCase(); - const description = post.description.toLowerCase(); - const tags = (post.tags || []).join(' ').toLowerCase(); - return title.includes(query) || description.includes(query) || tags.includes(query); - })); + setFilteredPosts( + allPosts.filter((post) => { + const title = post.title.toLowerCase(); + const description = post.description.toLowerCase(); + const tags = (post.tags || []).join(" ").toLowerCase(); + return ( + title.includes(query) || + description.includes(query) || + tags.includes(query) + ); + }), + ); } - }, [searchQuery]); + }, [searchQuery, allPosts]); const filterByTag = (tag: string) => { setSearchQuery(`#${tag}`); }; return ( -
- Blog
& Notes.} - description="A public notebook of things I figured out, mistakes I made, and tools I tested." +
+ + + + Blog
+ & Notes. + + } + description="Ein technisches Notizbuch über Lösungen, Fehler und Werkzeuge." + backLink={{ href: "/", label: "Zurück" }} backgroundSymbol="B" /> -
-
+
} + > +
{/* Sidebar / Filter area */} -
+
-
-

Suchen

+
+
{allTags.length > 0 && ( -
-

Themen

+
+
{allTags.map((tag, index) => ( @@ -84,11 +120,13 @@ export default function BlogPage() {
{/* Posts area */} -
-
+
+
{filteredPosts.length === 0 ? ( -
-

No posts found matching your criteria.

+
+

+ Keine Beiträge gefunden. +

) : ( filteredPosts.map((post, i) => ( @@ -100,7 +138,7 @@ export default function BlogPage() {
-
+
); } diff --git a/apps/web/app/case-studies/klz-cables/page.tsx b/apps/web/app/case-studies/klz-cables/page.tsx index 5920860..e8bc833 100644 --- a/apps/web/app/case-studies/klz-cables/page.tsx +++ b/apps/web/app/case-studies/klz-cables/page.tsx @@ -14,11 +14,13 @@ import { BodyText, } from "../../../src/components/Typography"; 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 { Activity, ArrowRight, + ArrowLeft, ShieldCheck, Cpu, Server, @@ -26,6 +28,7 @@ import { } from "lucide-react"; import { Marker } from "../../../src/components/Marker"; +import { GlitchText } from "../../../src/components/GlitchText"; export default function KLZCablesCaseStudy() { const { scrollYProgress } = useScroll(); @@ -50,6 +53,15 @@ export default function KLZCablesCaseStudy() { /> + + + {" "} + Zurück + +
@@ -71,13 +83,14 @@ export default function KLZCablesCaseStudy() {
- -

- KLZ Cables -
- Case Study. -

-
+ + KLZ Cables + +
+ Case Study.
@@ -126,14 +139,19 @@ export default function KLZCablesCaseStudy() { borderBottom containerVariant="normal" > + {/* Binary overlay background */} +
+ {Array.from({ length: 5 }).map((_, i) => ( +
01001101 01001001 01001110 01010100
+ ))} +
+
- -

- Architektur-
- Refactor. -

-
+

+ Architektur-
+ Refactor. +

@@ -217,9 +235,16 @@ export default function KLZCablesCaseStudy() { Infrastructure Validation

- Global Hub. + Global Hub.

+ + {/* Binary overlay left */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
HANDSHAKE_0x00{i}A // SYNC_ACTIVE
+ ))} +
@@ -298,12 +323,10 @@ export default function KLZCablesCaseStudy() { >
- -

- Fokus auf
- Spezifikationen. -

-
+

+ Fokus auf
+ Spezifikationen. +

@@ -443,43 +466,80 @@ export default function KLZCablesCaseStudy() { className="!pb-32" >
+
+ +
+ +

+ Direkter Draht. +

+ + Das Kontakt-System wurde auf maximale Reduktion getrimmt. Ein + deterministischer Kanal zwischen technischem Bedarf und + individueller Beratung – ohne Umwege, ohne Rauschen. + +
+
+ + +
+
+ + RESPONSE_TIME + +
+ < 120ms +
+
+
+ + PROTOCOL + +
+ mTLS +
+
+
+ + AVAILABILITY + +
+ 99.9% +
+
+
+ + ENCRYPTION + +
+ AES-256 +
+
+
+
+
+
- +
-
- -
- -

- Direkter Draht. -

- - Das Kontakt-System wurde auf maximale Reduktion getrimmt. - Keine unnötigen Hürden, sondern ein direkter - Kommunikations-Kanal zwischen technischem Bedarf und - individueller Beratung. - -
-
-
- {/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
@@ -540,15 +600,15 @@ export default function KLZCablesCaseStudy() {
- - Architektur-Audit anfragen + Jetzt anfragen - + diff --git a/apps/web/app/case-studies/page.tsx b/apps/web/app/case-studies/page.tsx index c50a1c2..a8531aa 100644 --- a/apps/web/app/case-studies/page.tsx +++ b/apps/web/app/case-studies/page.tsx @@ -1,104 +1,175 @@ "use client"; -import React from "react"; import { PageHeader } from "../../src/components/PageHeader"; import { Section } from "../../src/components/Section"; import { Reveal } from "../../src/components/Reveal"; -import { H3, LeadText, Label } from "../../src/components/Typography"; -import { BackgroundGrid, Card } from "../../src/components/Layout"; -import { MotionButton } from "../../src/components/Button"; -import Image from "next/image"; +import { H3, LeadText, BodyText, Label } from "../../src/components/Typography"; +import { Card } from "../../src/components/Layout"; +import { Button } from "../../src/components/Button"; +import { GradientMesh, AbstractCircuit } from "../../src/components/Effects"; +import { ArrowRight } from "lucide-react"; +import { motion } from "framer-motion"; export default function CaseStudiesPage() { return ( -
- +
+ - Case Studies:
- Qualität in jedem Detail. + Case Studies. } - 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" }} backgroundSymbol="C" /> -
-
- - -
- {/* We'll use a placeholder or a screenshot if available. - 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. */} -
- KLZ Cables Logo + {/* Featured Case Study */} +
} + > + + + + {/* Brand Gradient Background */} +
+ + {/* Left Column: Content */} +
+
+
+ KLZ Logo +
+ +
+ +
+

+ KLZ Cables +

+ + Engineering eines industriellen B2B-Systems mit + + {" "} + automatisierter Asset-Pipeline + {" "} + und hochperformantem Headless-Stack. + +
+ +
+ {["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map( + (tag, i) => ( + + {tag} + + ), + )} +
+
+ +
+
+ EXPLORE PROJECT + +
-
- -

- KLZ Cables – Digitaler Netzbau -

- - Wie wir eine komplexe WordPress-Struktur in ein performantes, - sauberes und langlebiges Web-System verwandelt haben. Fokus - auf Performance, SEO und Benutzerführung. - + {/* Right Column: Visual/Technical Decor */} +
+
+ {Array.from({ length: 40 }).map((_, i) => ( +
+ {Array.from({ length: 10 }) + .map((_, j) => ( + 0.5 + ? "text-slate-900" + : "text-slate-400" + } + > + {Math.floor(Math.random() * 2)} + + )) + .join(" ")} +
+ ))} +
-
- - Case Study lesen - + {/* Abstract "Cable" lines */} +
+
+ {[1, 2, 3].map((v) => ( + + ))} +
+
+ +
+ Industrial Grade
- - - -
- -

- Weitere Projekte sind in Arbeit. -

- - Ich dokumentiere gerade weitere spannende Projekte aus den - Bereichen SaaS, E-Commerce und Systemarchitektur. - -
-
-
+
+
-
-
- -

- Warum ich Case Studies zeige?
- - Weil Code mehr als Text ist. - -

-
- - - In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht - um die technischen Entscheidungen, die ein Projekt erfolgreich - machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind - keine Zufälle, sondern das Ergebnis von präziser Planung. - - -
+ {/* Coming Soon */} +
+ + +
+
+
+ +
+

+ Weitere Case Studies in Kürze. +

+ + Ich dokumentiere laufende Projekte – schauen Sie bald wieder + vorbei oder kontaktieren Sie mich direkt. + +
+ +
+
+ +
); diff --git a/apps/web/app/contact/page.tsx b/apps/web/app/contact/page.tsx index 8b0f0c9..b6408fb 100644 --- a/apps/web/app/contact/page.tsx +++ b/apps/web/app/contact/page.tsx @@ -1,42 +1,95 @@ -import * as React from "react"; -import { Reveal } from "../../src/components/Reveal"; import { PageHeader } from "../../src/components/PageHeader"; +import { Reveal } from "../../src/components/Reveal"; 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 { GradientMesh, AbstractCircuit } from "../../src/components/Effects"; export default function ContactPage() { return ( -
+
+ + - Projekt
- konfigurieren. + Kontakt. } - 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" }} - backgroundSymbol="?" + backgroundSymbol="@" /> -
- -
+
+ + + } + > +
+ {/* Form */} +
+ + + + + +
-
-
- - - + {/* Sidebar */} +
+ + +
+
+
+ +
+ + Aktuell nehme ich Projekte für{" "} + Q2 2026{" "} + an. + +
+
+
+ + + +
+ + + marc@mintel.me + +
+
+
+ + +
+ +

+ < 24h an Werktagen. +

+
+
+
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index a469747..427caa6 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -61,7 +61,7 @@ @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; } @@ -86,7 +86,6 @@ /* Components - Tailwind utility classes */ @layer components { - /* Legacy hooks required by tests */ .file-example { @apply w-full; @@ -180,7 +179,8 @@ /* Buttons */ .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 { @@ -266,7 +266,9 @@ color: #1e293b; border-color: #cbd5e1; 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 */ @@ -284,7 +286,6 @@ /* Print styles */ @media print { - .floating-back-to-top, .reading-progress-bar { display: none !important; @@ -343,27 +344,43 @@ } .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; } .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; } .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; } .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; } .highlighter-tag:hover::before { - content: ''; + content: ""; position: absolute; inset: -2px; background: inherit; @@ -385,7 +402,7 @@ /* Marker Title Styles */ .marker-title::before { - content: ''; + content: ""; position: absolute; left: -0.15em; right: -0.15em; @@ -394,22 +411,23 @@ border-radius: 0.18em; z-index: -1; - background: - linear-gradient(180deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0) 20%, - rgba(253, 230, 138, 0.70) 20%, - rgba(253, 230, 138, 0.70) 100%); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0) 20%, + rgba(253, 230, 138, 0.7) 20%, + rgba(253, 230, 138, 0.7) 100% + ); transform-origin: left center; - transform: - rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg)); + transform: rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) + skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg)); filter: saturate(1.05); } .marker-title::after { - content: ''; + content: ""; position: absolute; left: -0.18em; right: -0.05em; @@ -418,27 +436,28 @@ border-radius: 0.18em; z-index: -1; - background: - linear-gradient(90deg, - rgba(253, 230, 138, 0.00) 0%, - rgba(253, 230, 138, 0.60) 8%, - rgba(253, 230, 138, 0.55) 60%, - rgba(253, 230, 138, 0.35) 100%); + background: linear-gradient( + 90deg, + rgba(253, 230, 138, 0) 0%, + rgba(253, 230, 138, 0.6) 8%, + rgba(253, 230, 138, 0.55) 60%, + rgba(253, 230, 138, 0.35) 100% + ); opacity: 0.75; mix-blend-mode: multiply; - transform: - rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) translateY(0.02em); + transform: rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg)) + translateY(0.02em); - mask-image: - linear-gradient(180deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 1) 20%, - rgba(0, 0, 0, 1) 100%); + mask-image: linear-gradient( + 180deg, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 1) 20%, + rgba(0, 0, 0, 1) 100% + ); } @media (prefers-reduced-motion: no-preference) { - .post-link:hover .marker-title::before, .post-link:hover .marker-title::after { filter: saturate(1.08) contrast(1.02); @@ -480,7 +499,7 @@ .mermaid-container .edgeLabel, .mermaid-container .node text, .mermaid-container tspan { - font-family: 'Inter', sans-serif !important; + font-family: "Inter", sans-serif !important; fill: #334155 !important; color: #334155 !important; stroke: none !important; @@ -532,7 +551,9 @@ } .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; } @@ -602,20 +623,22 @@ color: #475569; } -.copy-btn[data-copied='true'] { +.copy-btn[data-copied="true"] { color: #065f46; - background: rgba(16, 185, 129, 0.10); + background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.35); } /* Prism.js syntax highlighting - light, low-noise */ -code[class*='language-'], -pre[class*='language-'], -pre:has(code[class*='language-']) { +code[class*="language-"], +pre[class*="language-"], +pre:has(code[class*="language-"]) { color: #0f172a; background: transparent; 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; line-height: 1.65; direction: ltr; @@ -689,4 +712,269 @@ pre:has(code[class*='language-']) { .token.important, .token.variable { color: #db2777; -} \ No newline at end of file +} + +/* ═══════════════════════════════════════════════ + 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; + } +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5a5e8a6..39576f3 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,18 +1,14 @@ +"use client"; + import { ComparisonRow, - ConceptAutomation, ConceptCode, ConceptCommunication, ConceptPrice, ConceptPrototyping, - ConceptSystem, ConceptWebsite, - DifferenceIllustration, - HeroArchitecture, - HeroMainIllustration, } from "../src/components/Landing"; import { Reveal } from "../src/components/Reveal"; -import { Marker } from "../src/components/Marker"; import { Section } from "../src/components/Section"; import { H1, @@ -22,172 +18,114 @@ import { MonoLabel, Label, } 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 { GradientMesh, CodeSnippet } from "../src/components/Effects"; 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() { return (
- + {/* Dark Hero */} + - {/* Hero Section */} -
- -
- {/* Left Column */} -
- -
-
-
- - Digital Architect - -
-

- Websites
- ohne Overhead. -

-
- -
-
-
-
+ {/* Rest of page on white */} - {/* Right Column */} -
-
- -
- - -
- -
-
-
-
-
-
- - {/* Section 02: The Promise */} -
+ {/* Section 02: The Promise – Streamlined */} +
} + >

- Schluss mit aufgeblähten Prozessen.
- - Ich reduziere auf das Wesentliche. + Kein Agentur-Zirkus.
+ + Nur{" "} + + Ergebnisse. +

-
- -
-
- -
- - {[ - { - text: "Direkte Kommunikation ohne Umwege", - icon: , - }, - { - text: "Schnelle Prototypen statt langer Konzepte", - icon: , - }, - { - text: "Sauberer Code, der auch morgen noch läuft", - icon: , - }, - { - text: "Fixpreise für volle Budgetsicherheit", - icon: , - }, - ].map((item, i) => ( - - {item.text} - - ))} - -
-
- - -
-
- -
- - {[ - "Endlose Workshops ohne Ergebnis", - "PowerPoint-Schlachten", - "Outsourcing an Billig-Anbieter", - "Wartungsverträge mit versteckten Kosten", - ].map((item, i) => ( - - - {item} - - - ))} - -
-
+
+ {[ + { + icon: , + title: "Direkte Kommunikation", + text: "Sie sprechen mit dem Entwickler. Keine Stille Post, keine Umwege.", + }, + { + icon: , + title: "Schnelle Umsetzung", + text: "Sichtbare Fortschritte in Tagen. Prototypen statt Konzeptpapiere.", + }, + { + icon: , + title: "Sauberer Code", + text: "Maßgeschneiderte Architektur. Kein Baukasten, kein Plugin-Chaos.", + }, + { + icon: , + title: "Klare Fixpreise", + text: "Volle Budgetsicherheit. Keine versteckten Kosten.", + }, + ].map((item, i) => ( + + +
+
+ {item.icon} +
+ + {item.text} +
+
+
+ ))}
- {/* Section 03: The Difference */} + {/* Section 03: The Difference – Visual Comparison */}
-
- - - Ich arbeite nicht gegen die Zeit, sondern{" "} - für das Ergebnis. Mein - Fokus liegt auf der Umsetzung, nicht auf der Verwaltung von - Prozessen. - - - -
- -
-
-
+ +

+ Ich arbeite für das Ergebnis,
+ + nicht gegen die{" "} + + Uhr. + + +

+
@@ -196,43 +134,40 @@ export default function LandingPage() {
{/* Section 04: Target Group */} -
+
- +
-
+
-

+

Unternehmer &
Geschäftsführer

- "Ich brauche eine Lösung, die funktioniert. Ich habe keine - Zeit für technische Details." + Sie wollen eine Website, die funktioniert – ohne sich mit + Technik beschäftigen zu müssen.
-
-
- {/* Section 05: Services */} + {/* Section 05: Leistungen — Interactive Service Rows */}
-
- - -
- -
-
-

Websites

- - High-Performance Websites. Maßgeschneiderte Architektur statt - Baukasten. - -
- - Details - +
+ {[ + { + num: "01", + binary: "00000001", + title: "Websites", + text: "High-Performance Websites mit maßgeschneiderter Architektur. Von der Konzeption bis zum Go-Live — individuell, schnell, messbar.", + tags: ["Next.js", "React", "TypeScript", "Performance"], + href: "/websites", + }, + { + num: "02", + binary: "00000010", + title: "Systeme", + text: "Web-Applikationen und interne Tools, wenn Standard-Software nicht reicht. Dashboards, Portale, Automatisierungen.", + tags: ["Full-Stack", "APIs", "Datenbanken", "Auth"], + href: "/contact", + }, + { + 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) => ( + +
+
+ {/* Number + Binary */} +
+ + {service.num} + + +
+ + {/* Content */} +
+

+ + {service.title} + +

+ + {service.text} + + {/* Tags */} +
+ {service.tags.map((tag, j) => ( + + {tag} + + ))} +
+
+ + {/* Arrow */} +
+ +
- -
- - - -
- -
-
-

Systeme

- - Web-Applikationen, Portale, interne Tools. Wenn Standard an - Grenzen stößt. - -
-
-
- - - -
- -
-
-

Automatisierung

- - Verbindung von Tools, automatische Prozesse, - Daten-Synchronisation. - -
-
-
+ + ))}
@@ -314,26 +277,26 @@ export default function LandingPage() {

Lassen Sie uns
- starten. + starten.

- Schreiben Sie mir kurz, worum es geht. Ich melde mich{" "} - zeitnah bei Ihnen. + Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "} + + zeitnah + {" "} + bei Ihnen.
-
+
diff --git a/apps/web/app/websites/page.tsx b/apps/web/app/websites/page.tsx index 4583e06..6fbb9c3 100644 --- a/apps/web/app/websites/page.tsx +++ b/apps/web/app/websites/page.tsx @@ -10,98 +10,129 @@ import { LayerSeparation, DirectService, TaskDone, - ConceptAutomation, - ConceptCode, - ConceptCommunication, - ConceptPrototyping, - ConceptSystem, - ConceptTarget, } from "../../src/components/Landing"; -import { Check } from "lucide-react"; import { - H2, H3, H4, LeadText, BodyText, Label, } from "../../src/components/Typography"; -import { BackgroundGrid, Card } from "../../src/components/Layout"; -import { MotionButton } from "../../src/components/Button"; +import { Card } from "../../src/components/Layout"; +import { Button } from "../../src/components/Button"; 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() { return (
- + Websites, die
- einfach funktionieren. + + + einfach funktionieren. + + } - 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" }} backgroundSymbol="W" /> - {/* Intro / Problem */} + {/* 01: Architektur – WIE ich baue */}
} >

- Ich baue Websites wie Systeme –
- nicht wie Broschüren. + Systeme, nicht Broschüren.
+ + Jede Website ist Ingenieursarbeit. +

- Eine Website ist kein Flyer. Sie ist ein{" "} - Werkzeug, das jeden Tag - arbeitet. Deshalb baue ich sie stabil, schnell und wartungsfrei. + Ich entwickle Websites von Grund auf – mit modernen Frameworks, + eigener Infrastruktur und einem Deployment-Prozess, der{" "} + + + automatisiert und reproduzierbar + + {" "} + ist. + + {/* Tech Stack Visual */} -
+
{[ - { label: "Stabil", icon: ConceptSystem }, - { label: "Schnell", icon: ConceptAutomation }, - { label: "Wartungsfrei", icon: ConceptCode }, - { label: "Sicher", icon: ConceptTarget }, + { label: "Next.js", sub: "Framework" }, + { label: "TypeScript", sub: "Sprache" }, + { label: "Docker", sub: "Infrastruktur" }, + { label: "Directus", sub: "CMS" }, ].map((item, i) => ( -
-
- + +
+ + + {item.sub} +
- -
+ ))}
+ + {/* Decorative Code Snippet */} + +
+ +
+
- {/* Speed */} + {/* 02: Performance */}
} + effects={} >

Geschwindigkeit ist
- - kein Extra. Sie ist Standard. + + + kein Extra. Sie ist Standard. +

@@ -109,19 +140,18 @@ export default function WebsitesPage() {
- Viele Websites sind langsam, weil sie zusammengeklickt sind. - Meine sind schnell, weil sie{" "} - von Grund auf{" "} - entwickelt wurden. + Jede Seite wird vorab gerendert und über ein CDN ausgeliefert. + Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "} + Reproduzierbar. {[ - "Seiten laden ohne Verzögerung", - "Optimiert für Suchmaschinen (SEO)", - "Bessere Nutzererfahrung", - "Höhere Conversion-Rates", + "Server-Side Rendering für sofortige Inhalte", + "Automatische Bild-Optimierung (WebP, AVIF)", + "Lighthouse-Score 90+ als Mindeststandard", + "Core Web Vitals im grünen Bereich", ].map((item, i) => ( {item} @@ -133,14 +163,18 @@ export default function WebsitesPage() {
90+
- + + + PERFORMANCE · ACCESSIBILITY · SEO +
@@ -148,10 +182,10 @@ export default function WebsitesPage() {
- {/* No Maintenance */} + {/* 03: Code-Qualität */}
} > @@ -159,91 +193,107 @@ export default function WebsitesPage() {

Keine Plugins.
- Keine Abhängigkeiten. + Keine Abhängigkeiten.

- Ich nutze keine Baukästen, die sich selbst zerstören. Ihre Website - besteht aus sauberem Code, - der Ihnen gehört. + Ihre Website besteht aus{" "} + Ihrem Code. Kein + WordPress, kein Wix, keine Blackbox. Alles versioniert, alles + nachvollziehbar. - -
- -
- -

Langlebigkeit

- - Modernste Web-Technologien für maximale Performance und - Wartbarkeit. - -
-
- -
- -

Resilienz

- - Minimale Angriffsfläche durch Verzicht auf unnötige - Drittanbieter-Software. - -
-
-
-
+ + {/* Git Branch Visualization */} +
+ + + + +
+ +
+ + + Jede Änderung ist dokumentiert. Rollbacks in Sekunden. + Kein „wer hat das kaputt gemacht?". + +
+
+ +
+ + + Code wird geprüft, getestet und automatisch live + geschaltet. Ohne manuellen Eingriff. + +
+
+
+
+
- {/* Content/Tech Separation */} + {/* 04: Content-System */}
} + effects={} >

Inhalte pflegen
- ohne Angst. + ohne Angst.

- Sie können Texte und Bilder selbst anpassen, ohne das Design - oder die Technik zu gefährden. Ein{" "} - intuitives System{" "} - sorgt dafür, dass alles an seinem Platz bleibt. + Technik und Inhalt sind{" "} + + + strikt getrennt + + + . Sie bearbeiten Texte und Bilder in einem intuitiven System – + das Design bleibt geschützt.
- -
- -
-
- -
- Inhalte flexibel verwalten + +
+
+
+
+ + Texte, Bilder und Inhalte frei bearbeiten. +
-
- -
-
- Design-Chaos -
-
- Technische Fehler -
+
+
+
+
+ + Design, Layout, Code-Struktur. +
@@ -252,90 +302,34 @@ export default function WebsitesPage() {
- {/* Simple Changes */} + {/* 05: Was Sie bekommen */}
} - > -
- -

- Änderungen sind
- Teil des Konzepts. -

-
- - - Ihr Business entwickelt sich weiter, Ihre Website auch.
- Keine komplizierten Prozesse, sondern{" "} - direkte Umsetzung Ihrer - Ideen. -
-
- -
- - -
-

Direkter Draht

- - Sie sprechen direkt mit dem Entwickler. Keine Stille Post. - -
-
- - -
-

Agile Anpassung

- - Schnelle Iterationen statt langer Wartezeiten. - -
-
-
-
-
-
- - {/* Result */} -
} >

- Eine Website, die
- einfach läuft. + Was Sie konkret
+ bekommen.

{[ { - title: "Kein Overhead", - desc: "Fokus auf das, was Ihre Kunden wirklich brauchen.", + title: "Ihr Code", + desc: "Vollständiger Quellcode, versioniert auf GitHub. Kein Vendor Lock-in.", }, { - title: "Volle Kontrolle", - desc: "Der Code gehört Ihnen, ohne Vendor Lock-in.", + title: "Ihre Infrastruktur", + desc: "Docker-Container, CI/CD-Pipeline, automatisches Deployment.", }, { - title: "Echte Performance", - desc: "Messbare Geschwindigkeit für bessere Ergebnisse.", + title: "Ihr CMS", + desc: "Eigenes Content-Management-System. Volle Kontrolle über Ihre Inhalte.", }, ].map((item, i) => ( @@ -353,12 +347,12 @@ export default function WebsitesPage() {
- + - Lassen Sie uns über Ihr nächstes Projekt sprechen. + Lassen Sie uns über Ihr Projekt sprechen.
- Projekt anfragen +
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index dae3127..a03c8d8 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -29,6 +29,15 @@ const nextConfig = { }, ]; }, + async redirects() { + return [ + { + source: '/case-studies/klz', + destination: '/case-studies/klz-cables', + permanent: true, + }, + ]; + }, }; export default withMintelConfig(nextConfig); diff --git a/apps/web/src/components/Button.tsx b/apps/web/src/components/Button.tsx index 5e9e337..32d475c 100644 --- a/apps/web/src/components/Button.tsx +++ b/apps/web/src/components/Button.tsx @@ -1,67 +1,166 @@ -import * as React from 'react'; -import { ArrowRight } from 'lucide-react'; -import { motion } from 'framer-motion'; -import Link from 'next/link'; +"use client"; + +import * as React from "react"; +import { motion } from "framer-motion"; +import { ArrowRight } from "lucide-react"; +import Link from "next/link"; interface ButtonProps { href: string; children: React.ReactNode; - variant?: 'primary' | 'outline'; + variant?: "primary" | "outline" | "ghost"; + size?: "normal" | "large"; className?: string; 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 = ({ href, children, - variant = 'primary', + variant = "primary", + size = "normal", 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 variants = { - 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 [hovered, setHovered] = React.useState(false); + const [displayText, setDisplayText] = React.useState(null); + const contentRef = React.useRef(null); - const content = ( - <> - {children} - {showArrow && } - + // Binary scramble on hover + React.useEffect(() => { + if (!hovered) { + 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 = { + normal: "px-8 py-4 text-[10px]", + large: "px-10 py-5 text-[11px]", + }; + + const variants: Record = { + 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 = ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + className={`${base} ${sizes[size]} ${variants[variant]} ${className}`} + > + {/* Binary stream hover overlay */} + + + {binaryStr} {binaryStr} + + + + {/* Shimmer line on hover (top edge) */} + + + {/* Content */} + + {displayText ?? children} + {showArrow && ( + + )} + + + ); + + if (href.startsWith("#")) { return ( - - {content} + { + e.preventDefault(); + document.querySelector(href)?.scrollIntoView({ behavior: "smooth" }); + }} + > + {inner} ); } - return ( - - {content} - - ); -}; - -export const MotionButton: React.FC = ({ - href, - children, - variant = 'primary', - className = "", - showArrow = true -}) => { - return ( - - - - ); + return {inner}; }; diff --git a/apps/web/src/components/Effects/AbstractCircuit.tsx b/apps/web/src/components/Effects/AbstractCircuit.tsx new file mode 100644 index 0000000..d7296e5 --- /dev/null +++ b/apps/web/src/components/Effects/AbstractCircuit.tsx @@ -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(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 ( + + ); +}; diff --git a/apps/web/src/components/Effects/BinaryStream.tsx b/apps/web/src/components/Effects/BinaryStream.tsx new file mode 100644 index 0000000..6b60246 --- /dev/null +++ b/apps/web/src/components/Effects/BinaryStream.tsx @@ -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 = ({ + 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") => ( +
+ {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 ( +
+ {generateColumn(offset + 42)} + {"\n"} + {generateColumn(offset + 99)} +
+ ); + })} +
+ ); + + return ( + + ); +}; diff --git a/apps/web/src/components/Effects/CircuitBoard.tsx b/apps/web/src/components/Effects/CircuitBoard.tsx new file mode 100644 index 0000000..358b669 --- /dev/null +++ b/apps/web/src/components/Effects/CircuitBoard.tsx @@ -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 = ({ + className = "", + density = "medium", + animate = true, +}) => { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const nodesRef = useRef([]); + const tracesRef = useRef([]); + const animationFrameRef = useRef(0); + const timeRef = useRef(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 ( + + ); +}; diff --git a/apps/web/src/components/Effects/CodeSnippet.tsx b/apps/web/src/components/Effects/CodeSnippet.tsx new file mode 100644 index 0000000..df5f4ce --- /dev/null +++ b/apps/web/src/components/Effects/CodeSnippet.tsx @@ -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 = ({ + className = "", + variant = "code", +}) => { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: "-10%" }); + const [visibleLineIndex, setVisibleLineIndex] = useState(-1); + const [displayText, setDisplayText] = useState([]); + + 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 ( + +
+ {variant === "code" && + codeLines.map((line, i) => ( + + {displayText[i] || ""} + {i === visibleLineIndex && ( + + )} + + ))} + + {variant === "git" && ( +
+
+ {gitBranches.map((item, i) => ( + +
+ {item.type === "branch" && ( + + {item.branch} + + )} + + {displayText[i] || ""} + + + ))} +
+ )} + + {variant === "terminal" && + terminalLines.map((line, i) => ( + + {line.prompt && ( + + )} + + {displayText[i] || ""} + + {i === visibleLineIndex && ( + + )} + + ))} +
+ + ); +}; diff --git a/apps/web/src/components/Effects/CodeWindow.tsx b/apps/web/src/components/Effects/CodeWindow.tsx new file mode 100644 index 0000000..894b719 --- /dev/null +++ b/apps/web/src/components/Effects/CodeWindow.tsx @@ -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 = ({ + title, + children, + className = "", + actions, + fixedHeight = false, + minHeight = "380px", +}) => { + return ( +
+ {/* Window chrome */} +
+
+
+
+
+ + {title} + +
+ {actions &&
{actions}
} +
+ + {/* Content area */} +
+ {children} +
+ + {/* Bottom gradient fade for aesthetics/depth */} +
+
+ ); +}; diff --git a/apps/web/src/components/Effects/DataFlow.tsx b/apps/web/src/components/Effects/DataFlow.tsx new file mode 100644 index 0000000..6f571ad --- /dev/null +++ b/apps/web/src/components/Effects/DataFlow.tsx @@ -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 = ({ + className = "", + lines = 3, + speed = "normal", +}) => { + const speedMap = { slow: "8s", normal: "5s", fast: "3s" }; + const duration = speedMap[speed]; + + return ( + + ); +}; diff --git a/apps/web/src/components/Effects/GradientMesh.tsx b/apps/web/src/components/Effects/GradientMesh.tsx new file mode 100644 index 0000000..0134ed1 --- /dev/null +++ b/apps/web/src/components/Effects/GradientMesh.tsx @@ -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 = ({ + 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 ( +