fix(web): correct relative imports in opengraph-image routes
Some checks failed
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 8m32s
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Failing after 1m33s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Failing after 8m32s
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Failing after 1m33s
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
This commit is contained in:
23
apps/web/app/(site)/about/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/about/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Über mich."
|
||||
description="15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen halten."
|
||||
label="Marc Mintel"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
338
apps/web/app/(site)/about/page.tsx
Normal file
338
apps/web/app/(site)/about/page.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import {
|
||||
ExperienceIllustration,
|
||||
ResponsibilityIllustration,
|
||||
ResultIllustration,
|
||||
ContactIllustration,
|
||||
HeroLines,
|
||||
ParticleNetwork,
|
||||
GridLines,
|
||||
} from "@/src/components/Landing";
|
||||
import { Signature } from "@/src/components/Signature";
|
||||
import { Check } from "lucide-react";
|
||||
import {
|
||||
H1,
|
||||
H3,
|
||||
H4,
|
||||
LeadText,
|
||||
BodyText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
} from "@/src/components/Typography";
|
||||
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 { getImgproxyUrl } from "@/src/utils/imgproxy";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
{/* Background decoration removed per user request */}
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative pt-12 md:pt-32 pb-8 md:pb-24 overflow-hidden border-b border-slate-50">
|
||||
<Container variant="narrow" className="relative z-10">
|
||||
<div className="flex flex-col items-center text-center space-y-6 md:space-y-12">
|
||||
<Reveal width="fit-content">
|
||||
<div className="relative">
|
||||
{/* Structural rings around avatar */}
|
||||
{/* Structural rings removed per user request */}
|
||||
|
||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full overflow-hidden border border-slate-200 shadow-xl bg-white p-1 group">
|
||||
<div className="w-full h-full rounded-full overflow-hidden relative aspect-square">
|
||||
<img
|
||||
src={getImgproxyUrl("/marc-mintel.png", {
|
||||
width: 400,
|
||||
height: 400,
|
||||
resizing_type: "fill",
|
||||
gravity: "sm",
|
||||
})}
|
||||
alt="Marc Mintel"
|
||||
className="object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0 w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-3 md:space-y-6 max-w-3xl">
|
||||
<Reveal delay={0.1}>
|
||||
<div className="flex items-center justify-center gap-2 md:gap-4 mb-1 md:mb-4">
|
||||
<div className="h-px w-6 md:w-8 bg-slate-900"></div>
|
||||
<MonoLabel className="text-slate-900 text-[10px] md:text-sm">
|
||||
Digital Architect
|
||||
</MonoLabel>
|
||||
<div className="h-px w-6 md:w-8 bg-slate-900"></div>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<H1 className="text-4xl md:text-8xl leading-none tracking-tighter">
|
||||
Über <span className="text-slate-400">mich.</span>
|
||||
</H1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<p className="text-slate-400 font-medium max-w-xl mx-auto text-sm md:text-xl">
|
||||
15 Jahre Erfahrung. Ein Ziel: Websites, die ihre Versprechen
|
||||
halten.
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
{/* Connector to first section */}
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-12 md:h-16 bg-gradient-to-b from-transparent to-slate-200" />
|
||||
</section>
|
||||
|
||||
{/* Section 01: Story */}
|
||||
<Section
|
||||
number="01"
|
||||
title="Erfahrung"
|
||||
borderTop
|
||||
illustration={<ExperienceIllustration className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Vom Designer <br />
|
||||
<span className="text-slate-400">zum Architekten.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
|
||||
<Reveal delay={0.1}>
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
<LeadText className="text-xl md:text-2xl text-slate-400">
|
||||
Agenturen, Konzerne, Startups – ich habe die Branche von allen
|
||||
Seiten kennengelernt. Was hängen geblieben ist:{" "}
|
||||
<span className="text-slate-900">
|
||||
<Marker delay={0.2}>Ergebnisse</Marker> zählen. Nicht der
|
||||
Weg dorthin.
|
||||
</span>
|
||||
</LeadText>
|
||||
<IconList className="space-y-4">
|
||||
{[
|
||||
"Frontend, Backend, Infrastruktur – Fullstack",
|
||||
"Komplexe Systeme auf das Wesentliche reduziert",
|
||||
"Performance-Probleme systematisch gelöst",
|
||||
].map((item, i) => (
|
||||
<IconListItem key={i} bullet>
|
||||
<BodyText className="text-lg">{item}</BodyText>
|
||||
</IconListItem>
|
||||
))}
|
||||
</IconList>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<Card
|
||||
variant="gray"
|
||||
hover={false}
|
||||
padding="normal"
|
||||
className="group"
|
||||
>
|
||||
<H4 className="text-xl mb-6">
|
||||
Heute: Direkte Zusammenarbeit ohne Reibungsverluste.
|
||||
</H4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{["Effizient", "Pragmatisch", "Verlässlich"].map((tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-4 py-2 bg-white border border-slate-200 rounded-full shadow-sm"
|
||||
>
|
||||
<Label className="text-slate-900">{tag}</Label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 02: Arbeitsweise – HOW I work */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Arbeitsweise"
|
||||
variant="gray"
|
||||
borderTop
|
||||
illustration={<ResponsibilityIllustration className="w-24 h-24" />}
|
||||
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
So läuft ein Projekt <br />
|
||||
<span className="text-slate-400">bei mir ab.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
{/* Timeline Steps */}
|
||||
<div className="space-y-1 relative">
|
||||
{/* Connecting line */}
|
||||
<div className="absolute left-[15px] top-8 bottom-8 w-px bg-slate-200 hidden md:block" />
|
||||
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.1}>
|
||||
<div className="flex gap-4 md:gap-6 py-2 md: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 className="space-y-1 md:space-y-2 pt-1">
|
||||
<H4 className="text-base md:text-xl font-bold">
|
||||
{item.title}
|
||||
</H4>
|
||||
<BodyText className="text-slate-500 text-sm md:text-base">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 03: Garantie – The Pledge */}
|
||||
<Section number="03" title="Garantie" borderTop>
|
||||
<div className="relative">
|
||||
<Reveal>
|
||||
<div className="max-w-4xl text-left space-y-12 md:space-y-16 py-8 md:py-16">
|
||||
<H3 className="text-3xl md:text-6xl leading-tight">
|
||||
Ich stehe für <br />
|
||||
<span className="text-slate-400">meine Arbeit gerade.</span>
|
||||
</H3>
|
||||
|
||||
<div className="prose prose-lg md:prose-2xl text-slate-500 leading-relaxed">
|
||||
<p>
|
||||
Keine Hierarchien. Keine Ausreden. Wenn etwas nicht passt,
|
||||
liegt die Verantwortung bei mir.
|
||||
</p>
|
||||
<p>
|
||||
Ich liefere nicht nur Code, sondern{" "}
|
||||
<span className="text-slate-900 font-medium relative inline-block">
|
||||
Ergebnisse
|
||||
<svg
|
||||
className="absolute -bottom-2 left-0 w-full h-3 text-blue-500/30"
|
||||
viewBox="0 0 100 10"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path
|
||||
d="M0 5 Q 50 10 100 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
, auf die Sie bauen können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-2xl text-left">
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<h4 className="font-bold text-slate-900 mb-2">
|
||||
Fixpreis-Garantie
|
||||
</h4>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Keine versteckten Kosten. Der vereinbarte Preis ist final.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
|
||||
<h4 className="font-bold text-slate-900 mb-2">
|
||||
Satisfaction Guarantee
|
||||
</h4>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Wir gehen erst live, wenn Sie zu 100% zufrieden sind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-8 md:pt-12 flex flex-col items-start">
|
||||
<div className="w-64 md:w-80">
|
||||
<Signature delay={0.5} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 04: CTA */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Kontakt"
|
||||
variant="gray"
|
||||
borderTop
|
||||
illustration={<ContactIllustration className="w-24 h-24" />}
|
||||
effects={<GradientMesh variant="metallic" className="opacity-60" />}
|
||||
>
|
||||
<div className="space-y-10 md:space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Bereit für eine <br />
|
||||
<span className="text-slate-400">Zusammenarbeit?</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<Card
|
||||
variant="glass"
|
||||
hover={false}
|
||||
padding="normal"
|
||||
techBorder
|
||||
className="rounded-3xl shadow-xl relative overflow-hidden group"
|
||||
>
|
||||
<div className="relative z-10 space-y-6 md:space-y-8">
|
||||
<LeadText className="text-lg md:text-4xl leading-tight max-w-2xl text-slate-400">
|
||||
Lassen Sie uns gemeinsam etwas bauen, das{" "}
|
||||
<span className="text-slate-900">
|
||||
wirklich <Marker delay={0.3}>funktioniert.</Marker>
|
||||
</span>
|
||||
</LeadText>
|
||||
|
||||
<div className="pt-2 md:pt-4">
|
||||
<Button href="/contact" className="w-full md:w-auto">
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
70
apps/web/app/(site)/blog/[slug]/opengraph-image.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getAllPosts } from "../../../../src/lib/posts";
|
||||
import { blogThumbnails } from "../../../../src/components/blog/blogThumbnails";
|
||||
import { BlogOGImageTemplate } from "../../../../src/components/BlogOGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../../src/lib/og-helper";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
let backgroundImageSrc: string | undefined = undefined;
|
||||
|
||||
// If we have a custom generated thumbnail, convert it to a data URI for Satori
|
||||
if (post?.thumbnail) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), "public", post.thumbnail);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(post.thumbnail).substring(1).toLowerCase();
|
||||
const mimeType =
|
||||
ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
||||
|
||||
backgroundImageSrc = `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[OG Image Generator] Could not read thumbnail file for ${slug} to use as background:`,
|
||||
err,
|
||||
);
|
||||
// Fall through to standard plain background
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnail = blogThumbnails[slug];
|
||||
|
||||
const title = post?.title || "Marc Mintel";
|
||||
const description =
|
||||
post?.description ||
|
||||
"Technical problem solver's blog - practical insights and learning notes";
|
||||
const label = post ? "Blog Post" : "Engineering";
|
||||
const accentColor = thumbnail?.accent;
|
||||
const keyword = thumbnail?.keyword;
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<BlogOGImageTemplate
|
||||
title={title}
|
||||
description={description}
|
||||
label={label}
|
||||
accentColor={accentColor}
|
||||
keyword={keyword}
|
||||
backgroundImageSrc={backgroundImageSrc}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
115
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
115
apps/web/app/(site)/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogPostHeader } from "@/src/components/blog/BlogPostHeader";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { BlogPostClient } from "@/src/components/BlogPostClient";
|
||||
import { TextSelectionShare } from "@/src/components/TextSelectionShare";
|
||||
import { BlogPostStickyBar } from "@/src/components/blog/BlogPostStickyBar";
|
||||
import { MDXContent } from "@/src/components/MDXContent";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
return allPosts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) return {};
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
type: "article",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const post = allPosts.find((p) => p.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const formattedDate = new Date(post.date).toLocaleDateString("de-DE", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const wordCount = post.description.split(/\s+/).length + 300;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 md:gap-12 py-8 md:py-24 overflow-hidden">
|
||||
<BlogPostClient readingTime={readingTime} title={post.title} />
|
||||
|
||||
<BlogPostHeader
|
||||
title={post.title}
|
||||
description={post.description}
|
||||
date={formattedDate}
|
||||
readingTime={readingTime}
|
||||
slug={slug}
|
||||
thumbnail={post.thumbnail}
|
||||
/>
|
||||
|
||||
<main id="post-content">
|
||||
<BlogPostStickyBar
|
||||
title={post.title}
|
||||
url={`https://mintel.me/blog/${slug}`}
|
||||
/>
|
||||
|
||||
<Section containerVariant="wide" className="pt-0 md:pt-0">
|
||||
<div className="max-w-4xl mx-auto px-5 md:px-0">
|
||||
<Reveal delay={0.4} width="100%">
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-10 md:mb-12">
|
||||
{post.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-slate-50 border border-slate-100 rounded text-[9px] md:text-[10px] font-mono text-slate-500 uppercase tracking-widest"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="article-content max-w-none">
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
|
||||
<TextSelectionShare />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
apps/web/app/(site)/blog/page.tsx
Normal file
14
apps/web/app/(site)/blog/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { BlogClient } from "@/src/components/blog/BlogClient";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | Mintel.me",
|
||||
description:
|
||||
"Gedanken über Engineering, Design und die Architektur der Zukunft.",
|
||||
};
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getAllPosts();
|
||||
return <BlogClient allPosts={posts as any} />;
|
||||
}
|
||||
640
apps/web/app/(site)/case-studies/klz-cables/page.tsx
Normal file
640
apps/web/app/(site)/case-studies/klz-cables/page.tsx
Normal file
@@ -0,0 +1,640 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
LeadText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
BodyText,
|
||||
} from "@/src/components/Typography";
|
||||
import { BackgroundGrid, Container } from "@/src/components/Layout";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IframeSection } from "@/src/components/IframeSection";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ShieldCheck,
|
||||
Cpu,
|
||||
Server,
|
||||
Layers,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
import { GlitchText } from "@/src/components/GlitchText";
|
||||
|
||||
export default function KLZCablesCaseStudy() {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const heroY = useTransform(scrollYProgress, [0, 0.2], [0, -20]);
|
||||
const heroOpacity = useTransform(scrollYProgress, [0, 0.15], [1, 0]);
|
||||
const gridRotate = useTransform(scrollYProgress, [0, 1], [0, 2]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white relative min-h-screen selection:bg-slate-900 selection:text-white overflow-hidden">
|
||||
<motion.div
|
||||
style={{ opacity: heroOpacity }}
|
||||
className="fixed inset-0 z-0 pointer-events-none"
|
||||
>
|
||||
<BackgroundGrid />
|
||||
</motion.div>
|
||||
|
||||
{/* --- HERO: INDUSTRIAL INFRASTRUCTURE --- */}
|
||||
<section className="relative min-h-[30vh] md:min-h-[40vh] py-10 md:py-20 overflow-hidden border-b border-slate-100 bg-white">
|
||||
<motion.div
|
||||
style={{ y: heroY, rotate: gridRotate }}
|
||||
className="absolute inset-0 bg-[linear-gradient(to_right,#f1f5f9_1px,transparent_1px),linear-gradient(to_bottom,#f1f5f9_1px,transparent_1px)] bg-[size:6rem_6rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_20%,transparent_100%)] pointer-events-none opacity-40"
|
||||
/>
|
||||
|
||||
<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-8 md: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">
|
||||
<Reveal direction="down" blur>
|
||||
<div className="inline-flex items-center gap-6">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
whileInView={{ width: 48 }}
|
||||
transition={{ duration: 1, ease: "circOut" }}
|
||||
className="h-px bg-slate-900"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-slate-900 tracking-[0.4em]">
|
||||
SYSTEM-ARCHITEKTUR // 2025
|
||||
</MonoLabel>
|
||||
<Label className="text-[10px] text-slate-400 font-mono">
|
||||
HARDENED WORDPRESS // VARNISH STACK
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<GlitchText
|
||||
as="h1"
|
||||
className="text-4xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900"
|
||||
>
|
||||
KLZ Cables
|
||||
</GlitchText>
|
||||
<br className="hidden md:block" />
|
||||
<span className="text-slate-100 text-3xl md:text-6xl font-bold tracking-tighter">
|
||||
Case Study.
|
||||
</span>
|
||||
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-6 md:pl-12">
|
||||
<LeadText className="text-lg md:text-4xl leading-tight text-slate-900 font-medium">
|
||||
Engineering eines <br className="hidden md:block" />
|
||||
<Marker delay={0.2}>Systems.</Marker>
|
||||
</LeadText>
|
||||
<BodyText className="mt-4 md:mt-6 text-base md:text-xl text-slate-500 max-w-xl leading-relaxed font-serif italic">
|
||||
Vom statischen Altsystem zum industriellen Standard. Ich
|
||||
habe das KLZ-System auf das Wesentliche reduziert: Hardened
|
||||
Infrastructure, parametrische Datenpflege und zero
|
||||
maintenance.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.4} direction="up" scale={0.98} blur>
|
||||
<div className="flex flex-wrap gap-8 md:gap-24 pt-6 md:pt-12 border-t border-slate-100">
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label className="text-slate-400 text-[10px] md:text-xs">
|
||||
Data Integrity
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="w-2 h-2 md:w-2.5 md:h-2.5 bg-[rgba(129,199,132,1)] rounded-full animate-pulse" />
|
||||
<span className="text-base md:text-2xl font-bold font-mono text-slate-900 tracking-tight">
|
||||
Relational Data
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label className="text-slate-400 text-[10px] md:text-xs">
|
||||
Security Layer
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 md:gap-3 text-base md:text-2xl font-bold font-mono text-slate-900">
|
||||
<ShieldCheck className="w-5 h-5 md:w-6 md:h-6 text-[rgba(129,199,132,1)]" />
|
||||
<span>WP + Varnish</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 01: ARCHITECTURE --- */}
|
||||
<Section
|
||||
number="01"
|
||||
title="System-Hardening & Logic"
|
||||
borderBottom
|
||||
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-8 lg:gap-24 items-start">
|
||||
<div className="md:col-span-12 mb-4 md:mb-12">
|
||||
<H2 className="text-3xl md:text-8xl tracking-tighter mb-4 md:mb-12">
|
||||
<GlitchText>Architektur-</GlitchText> <br />
|
||||
Refactor.
|
||||
</H2>
|
||||
</div>
|
||||
<div className="md:col-span-7 space-y-8 md:space-y-12">
|
||||
<Reveal delay={0.1} direction="up" blur>
|
||||
<div className="space-y-6 md:space-y-10">
|
||||
<BodyText className="text-xl md:text-2xl leading-relaxed font-serif italic text-slate-500">
|
||||
Vom statischen HTML zur zentralen Daten-Instanz.
|
||||
</BodyText>
|
||||
<BodyText className="text-lg md:text-xl text-slate-600 leading-relaxed">
|
||||
Ich habe die KLZ-Architektur radikal auf einen entkoppelten
|
||||
High-Performance-Stack umgestellt. WordPress fungiert hier
|
||||
nicht als CMS-Baukasten, sondern speichert alle technischen
|
||||
Attribute in einer zentralen relationalen Instanz. Durch die
|
||||
Implementierung nativer PHP-Services und den Verzicht auf
|
||||
volatile Drittanbieter-Plugins wurde ein System geschaffen,
|
||||
das keine technologischen Überraschungen zulässt. Stability by{" "}
|
||||
<Marker delay={0.5}>Design.</Marker>
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5 relative mt-8 md:mt-0">
|
||||
<Reveal delay={0.3} direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ y: -5, scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 25 }}
|
||||
className="p-6 md:p-12 bg-slate-50 rounded-[2.5rem] md:rounded-[3rem] border border-slate-100 space-y-6 md:space-y-12 relative overflow-hidden group shadow-sm text-left"
|
||||
>
|
||||
<div className="space-y-6 md:space-y-8 relative z-10">
|
||||
<Label className="text-slate-900">System Metriken</Label>
|
||||
<div className="space-y-6 md:space-y-8">
|
||||
{[
|
||||
{
|
||||
label: "Edge Caching",
|
||||
desc: "Varnish + W3TC Object Cache",
|
||||
icon: <Server className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
desc: "Independent (Global Data Compliance)",
|
||||
icon: <Activity className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
{
|
||||
label: "Custom Core",
|
||||
desc: "REST via Native Services",
|
||||
icon: <Cpu className="w-5 h-5 text-slate-400" />,
|
||||
},
|
||||
].map((item, i) => (
|
||||
<Reveal
|
||||
key={i}
|
||||
direction="right"
|
||||
delay={0.5 + i * 0.1}
|
||||
width="100%"
|
||||
className="flex gap-4 md:gap-6 border-b border-slate-200/50 pb-6 last:border-0 last:pb-0"
|
||||
>
|
||||
<div className="shrink-0 mt-1">{item.icon}</div>
|
||||
<div className="space-y-1">
|
||||
<MonoLabel className="text-[9px] md:text-[10px] text-slate-400">
|
||||
{item.label}
|
||||
</MonoLabel>
|
||||
<BodyText className="text-base font-bold text-slate-900">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SHOWCASE: LANDING --- */}
|
||||
<section className="py-12 md:py-32 bg-slate-50 border-y border-slate-100 overflow-hidden relative">
|
||||
<div className="w-full max-w-[1920px] mx-auto px-4 md:px-12 relative z-10">
|
||||
<Reveal direction="none" blur>
|
||||
<div className="relative mb-8 md:mb-16 flex justify-between items-end">
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-500">
|
||||
Infrastructure Validation
|
||||
</Label>
|
||||
<H3 className="text-3xl md:text-8xl tracking-tighter">
|
||||
<GlitchText>Global Hub.</GlitchText>
|
||||
</H3>
|
||||
</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>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/index.html"
|
||||
height="400px"
|
||||
mobileHeight="350px"
|
||||
desktopHeight="850px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full transition-all duration-1000 ease-in-out no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- SECTION 02: TECHNICAL DETAIL --- */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Asset Management"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-8 md:gap-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-24 items-end">
|
||||
<div className="md:col-span-8">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6 md:space-y-12">
|
||||
<Label className="text-slate-400 text-xs md:text-sm">
|
||||
Asset Pipelines
|
||||
</Label>
|
||||
<H3 className="text-2xl md:text-6xl tracking-tighter">
|
||||
Automated Documentation.
|
||||
</H3>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<Reveal delay={0.1} direction="right" blur>
|
||||
<BodyText className="text-lg md:text-xl text-slate-500 pb-2 font-serif italic leading-relaxed">
|
||||
Für Hochspannungs-N2XS(F)2Y Kabel ist Datentreue eine
|
||||
Sicherheitsanforderung. Ich habe eine automatisierte
|
||||
Asset-Pipeline entwickelt, die technische Datenblätter
|
||||
serverseitig generiert und validiert.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative w-full group"
|
||||
>
|
||||
<div className="relative w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/power-cables-medium-voltage-cables.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="1000px"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
offsetY={100}
|
||||
browserFrame
|
||||
className="w-full transition-all duration-1000 no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
number="03"
|
||||
title="Katalog-Architektur"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16 items-center">
|
||||
<div className="lg:col-span-12 mb-4 md:mb-12 text-center lg:text-left relative z-10">
|
||||
<H3 className="text-3xl md:text-6xl max-w-4xl tracking-tighter">
|
||||
Fokus auf <br />
|
||||
<Marker delay={0.2}>Spezifikationen.</Marker>
|
||||
</H3>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-8 relative group">
|
||||
<Reveal width="100%" direction="left" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[400px] md:h-[650px] w-full overflow-hidden group"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/which-cables-for-wind-power-differences-from-low-to-extra-high-voltage-explained-2.html"
|
||||
height="100%"
|
||||
desktopWidth={1920}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full transition-all duration-700 no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-10 relative z-10">
|
||||
<Reveal delay={0.2} direction="right" blur>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<Label className="text-slate-400">Katalog-Struktur</Label>
|
||||
<LeadText className="text-base md:text-lg leading-relaxed">
|
||||
Der Produktbereich wurde konsequent auf die Bedürfnisse
|
||||
technischer Planer optimiert. Klare Hierarchien und der
|
||||
Verzicht auf E-Commerce-Rauschen ermöglichen einen direkten
|
||||
Zugriff auf Kabel-Parameter und Datenblätter.
|
||||
</LeadText>
|
||||
<motion.div
|
||||
whileHover={{ x: 10 }}
|
||||
className="p-5 md:p-8 bg-white border border-slate-200 rounded-2xl md:rounded-3xl shadow-sm"
|
||||
>
|
||||
<Layers className="w-5 h-5 md:w-6 md:h-6 text-slate-400 mb-3 md:mb-4" />
|
||||
<BodyText className="text-sm font-medium">
|
||||
Strukturierte Aufbereitung technischer Produktdaten.
|
||||
</BodyText>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 04: CONTENT ENGINE --- */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Content Strategy"
|
||||
variant="white"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<div className="lg:col-span-4 space-y-10 order-2 lg:order-1">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Knowledge Transfer</Label>
|
||||
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
||||
Insights & News.
|
||||
</H3>
|
||||
<BodyText className="text-xl text-slate-500 font-serif italic">
|
||||
Die News-Engine dient als technischer Hub für
|
||||
Industrie-Standards. Durch die Implementierung eines
|
||||
performanten Blog-Systems wird Fachwissen direkt an die
|
||||
Zielgruppe kommuniziert.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="lg:col-span-8 order-1 lg:order-2">
|
||||
<Reveal width="100%" direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
className="relative h-[450px] md:h-[700px] w-full"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/blog.html"
|
||||
height="100%"
|
||||
desktopWidth={1600}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="h-full w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 05: TEAM & TRUST --- */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Ergebnis"
|
||||
borderBottom
|
||||
containerVariant="wide"
|
||||
>
|
||||
<div className="space-y-10 md:space-y-16 text-center">
|
||||
<Reveal direction="up" blur>
|
||||
<H3 className="text-3xl md:text-8xl tracking-tighter">
|
||||
System-Lifecycle.
|
||||
</H3>
|
||||
<LeadText className="mx-auto max-w-2xl pt-2 md:pt-6 text-lg md:text-xl leading-relaxed">
|
||||
Die Migration von einer statischen Datei-Struktur zu einer
|
||||
zentralisierten Daten-Instanz eliminiert technische Schulden und
|
||||
manuelle Fehlerquellen. Das Ergebnis ist eine wartungsfreie
|
||||
Architektur, die technische Datentreue über den gesamten
|
||||
Produkt-Lifecycle sicherstellt.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2} width="100%" direction="up" scale={0.98} blur>
|
||||
<div className="relative group w-full text-left">
|
||||
<div className="relative block w-full overflow-visible">
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/team.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="1100px"
|
||||
desktopWidth={1440}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full h-full no-scrollbar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* --- SECTION 06: CONVERSION --- */}
|
||||
<Section
|
||||
number="06"
|
||||
title="Lead Engineering"
|
||||
variant="white"
|
||||
containerVariant="wide"
|
||||
className="!pb-32"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-24 items-center">
|
||||
<div className="md:col-span-6 md:order-2">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-6">
|
||||
<Label className="text-slate-400">Interaction Layer</Label>
|
||||
<H3 className="text-3xl md:text-7xl tracking-tighter text-slate-900 leading-[1.1]">
|
||||
Direkter Draht.
|
||||
</H3>
|
||||
<BodyText className="text-lg md: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">
|
||||
<Reveal direction="right" scale={0.98} blur>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.002 }}
|
||||
transition={{ type: "spring", stiffness: 100, damping: 30 }}
|
||||
className="relative rounded-[2.5rem] overflow-hidden shadow-2xl shadow-slate-200/50 ring-1 ring-slate-100"
|
||||
>
|
||||
<IframeSection
|
||||
src="/showcase/klz-cables.com/contact.html"
|
||||
height="450px"
|
||||
mobileHeight="400px"
|
||||
desktopHeight="750px"
|
||||
desktopWidth={1200}
|
||||
mobileWidth={390}
|
||||
allowScroll
|
||||
browserFrame
|
||||
className="w-full no-scrollbar"
|
||||
/>
|
||||
</motion.div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/* --- FINAL CTA: ARCHITECTURE & VALUE --- */}
|
||||
<section className="py-40 md:py-64 bg-white relative overflow-hidden border-t border-slate-100">
|
||||
<BackgroundGrid />
|
||||
<Container variant="normal" className="relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center text-left">
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<Reveal direction="left" blur>
|
||||
<div className="space-y-3 md:space-y-6">
|
||||
<MonoLabel className="text-slate-400 tracking-[0.2em] md:tracking-[0.4em]">
|
||||
CONSULTING // ENGINEERING
|
||||
</MonoLabel>
|
||||
<H2 className="text-4xl md:text-8xl tracking-tighter leading-none font-bold">
|
||||
Architektur <br />
|
||||
<span className="text-slate-100">ohne Altlasten.</span>
|
||||
</H2>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2} direction="left" blur>
|
||||
<BodyText className="text-xl md:text-2xl text-slate-500 max-w-xl font-serif italic leading-relaxed">
|
||||
Vom Prototyp zum industriellen Standard. Ich entwickle
|
||||
digitale Infrastrukturen, die technische Freiheit und
|
||||
operative Stabilität garantieren – wartungsfrei und
|
||||
skalierbar.
|
||||
</BodyText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50/80 backdrop-blur-sm border border-slate-100 p-6 md:p-14 rounded-[3.5rem] space-y-10 md:space-y-12 relative group shadow-sm">
|
||||
<Reveal direction="right" blur>
|
||||
<div className="inline-flex items-center gap-2 md:gap-3 px-3 py-1.5 md:px-4 md:py-2 bg-white rounded-full border border-slate-200 mb-4 md:mb-8 font-mono text-[9px] md:text-[10px] tracking-widest text-slate-500 uppercase">
|
||||
<div className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
|
||||
Operational Excellence
|
||||
</div>
|
||||
<div className="space-y-8 md:space-y-10">
|
||||
{[
|
||||
{
|
||||
title: "Hardened Infrastructure",
|
||||
desc: "Zentralisierte Datenpflege und entkoppelte WordPress-Instanzen.",
|
||||
},
|
||||
{
|
||||
title: "Automated Data Pipelines",
|
||||
desc: "Validierung technischer Spezifikationen ohne manuelle Eingriffe.",
|
||||
},
|
||||
{
|
||||
title: "Maintenance-Free Core",
|
||||
desc: "Plugin-freie Logik für deterministische System-Sicherheit.",
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="space-y-2 md:space-y-3 group/item text-left"
|
||||
>
|
||||
<MonoLabel className="text-[9px] md:text-[10px] text-slate-400 group-hover/item:text-slate-900 transition-colors duration-500">
|
||||
{item.title}
|
||||
</MonoLabel>
|
||||
<BodyText className="text-base md:text-lg font-bold text-slate-900 leading-tight">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.5} direction="up" blur className="pt-6">
|
||||
<Button
|
||||
href="/contact"
|
||||
variant="outline"
|
||||
showArrow={false}
|
||||
className="w-full py-6 md:py-8 text-base md:text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700"
|
||||
>
|
||||
Jetzt anfragen
|
||||
<ArrowRight className="inline-block ml-3 md:ml-4 w-5 h-5 md:w-6 md:h-6 group-hover:translate-x-4 transition-transform duration-700" />
|
||||
</Button>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
apps/web/app/(site)/case-studies/page.tsx
Normal file
178
apps/web/app/(site)/case-studies/page.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { H3, LeadText, Label, BodyText } from "@/src/components/Typography";
|
||||
import { Card } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function CaseStudiesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
|
||||
{/* Featured Case Study Hero */}
|
||||
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||
<div className="space-y-12 md:space-y-24">
|
||||
<Reveal>
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<H3 className="text-4xl md:text-8xl tracking-tighter leading-none">
|
||||
Case <span className="text-slate-400">Studies.</span>
|
||||
</H3>
|
||||
<LeadText className="text-lg md:text-2xl text-slate-400 max-w-2xl">
|
||||
Ergebnisse statt Versprechen. Dokumentierte Architektur-Lösungen
|
||||
für komplexe Anforderungen.
|
||||
</LeadText>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal>
|
||||
<a href="/case-studies/klz-cables" className="block group">
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="none"
|
||||
techBorder
|
||||
className="overflow-hidden relative group min-h-[400px] md:min-h-[500px] flex flex-col md:flex-row"
|
||||
>
|
||||
{/* 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-4 md:p-12 relative z-10 flex flex-col justify-between">
|
||||
<div className="space-y-4 md:space-y-8">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<Image
|
||||
src="/showcase/klz-cables.com/assets/klz-cables.com/wp-content/uploads/2024/11/white_logo_transparent_background.svg"
|
||||
alt="KLZ Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-6 md:h-8 w-auto invert opacity-80 group-hover:opacity-100 transition-opacity duration-500"
|
||||
/>
|
||||
<div className="h-px w-8 md:w-12 bg-slate-100" />
|
||||
<Label className="text-slate-400 text-[9px] md:text-[10px]">
|
||||
Case Study 2025
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<H3 className="text-3xl md:text-6xl tracking-tighter">
|
||||
KLZ <span className="text-slate-300">Cables</span>
|
||||
</H3>
|
||||
<LeadText className="text-slate-500 text-base 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-1 md:pt-2">
|
||||
{["Next.js", "Varnish", "Asset Pipeline", "B2B DB"].map(
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-2 py-0.5 md:px-2.5 md:py-1 border border-slate-100 bg-white/50 rounded-md text-[8px] 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-8 md:pt-12">
|
||||
<div className="inline-flex items-center gap-2 md:gap-3 text-[10px] md: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>
|
||||
|
||||
{/* Right Column: Visual/Technical Decor */}
|
||||
<div className="w-full md:w-1/3 min-h-[150px] md:min-h-0 bg-slate-50 relative overflow-hidden border-t md:border-t-0 md:border-l border-slate-100">
|
||||
<div className="absolute inset-0 opacity-[0.03] select-none pointer-events-none font-mono text-[6px] md:text-[8px] p-4 flex flex-col gap-1 overflow-hidden">
|
||||
{Array.from({ length: 40 }).map((_, i) => (
|
||||
<div key={i} className="whitespace-nowrap">
|
||||
{Array.from({ length: 15 })
|
||||
.map((_, j) => (
|
||||
<span
|
||||
key={j}
|
||||
className={
|
||||
Math.random() > 0.5
|
||||
? "text-slate-900"
|
||||
: "text-slate-400"
|
||||
}
|
||||
>
|
||||
{Math.floor(Math.random() * 2)}
|
||||
</span>
|
||||
))
|
||||
.join(" ")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Abstract "Cable" lines */}
|
||||
<div className="absolute inset-0 flex items-center justify-center p-8 md:p-12">
|
||||
<div className="w-full h-full relative">
|
||||
{[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-4 right-4 md:bottom-8 md:right-8 text-[8px] md:text-[10px] font-mono text-slate-300 rotate-90 origin-right uppercase tracking-[0.3em]">
|
||||
Industrial Grade
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Coming Soon */}
|
||||
<Section number="02" title="Kommt bald" borderTop>
|
||||
<Reveal>
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="large"
|
||||
techBorder
|
||||
className="text-center relative overflow-hidden group"
|
||||
>
|
||||
<div className="relative z-10 space-y-4 py-4 md:py-8">
|
||||
<div className="flex items-center justify-center gap-2 md:gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-slate-300 animate-pulse" />
|
||||
<Label className="text-slate-400">In Arbeit</Label>
|
||||
</div>
|
||||
<H3 className="text-2xl md:text-3xl text-slate-400">
|
||||
Weitere Case Studies in Kürze.
|
||||
</H3>
|
||||
<BodyText className="text-slate-400 max-w-md mx-auto">
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/web/app/(site)/contact/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/contact/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Kontakt."
|
||||
description="Bereit für eine Zusammenarbeit? Lassen Sie uns gemeinsam etwas bauen, das wirklich funktioniert."
|
||||
label="Get in touch"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
20
apps/web/app/(site)/contact/page.tsx
Normal file
20
apps/web/app/(site)/contact/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Section } from "@/src/components/Section";
|
||||
import { ContactForm } from "@/src/components/ContactForm";
|
||||
import { AbstractCircuit } from "@/src/components/Effects";
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white min-h-screen overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
|
||||
<Section
|
||||
containerVariant="wide"
|
||||
effects={<></>}
|
||||
className="pt-24 pb-12 md:pt-32 md:pb-20"
|
||||
>
|
||||
{/* Full-width Form */}
|
||||
<ContactForm />
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
apps/web/app/(site)/error.tsx
Normal file
67
apps/web/app/(site)/error.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { Button } from "@/src/components/Button";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to Sentry/GlitchTip
|
||||
Sentry.captureException(error);
|
||||
console.error("Caught in error.tsx:", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-red-50 border border-red-100 rounded text-[10px] font-mono text-red-500 uppercase tracking-widest">
|
||||
Error 500
|
||||
</span>
|
||||
<h1 className="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter">
|
||||
Kritischer Fehler.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-slate-500 font-serif italic max-w-xl mx-auto leading-relaxed">
|
||||
Ein unerwartetes Problem ist aufgetreten. Unsere Systeme haben den
|
||||
Vorfall protokolliert.
|
||||
</p>
|
||||
|
||||
<div className="pt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button href="#" onClick={() => reset()} variant="primary">
|
||||
System neu starten (Retry)
|
||||
</Button>
|
||||
<Button href="/" variant="outline">
|
||||
Zurück zur Basis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-16 max-w-sm mx-auto">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100 text-left font-mono text-xs text-slate-400">
|
||||
<div className="flex items-center justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>STATUS</span>
|
||||
<span className="text-red-500 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span>
|
||||
FAIL
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>TRACKING</span>
|
||||
<span className="text-green-500">GLITCHTIP_LOGGED</span>
|
||||
</div>
|
||||
{error.digest && (
|
||||
<div className="flex justify-between">
|
||||
<span>DIGEST</span>
|
||||
<span className="truncate max-w-[150px]">{error.digest}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
apps/web/app/(site)/errors/api/relay/route.ts
Normal file
55
apps/web/app/(site)/errors/api/relay/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Smart Proxy / Relay for Sentry/GlitchTip events.
|
||||
*
|
||||
* Mirroring the klz-2026 pattern:
|
||||
* Receives Sentry envelopes from the client, injects the correct DSN,
|
||||
* and forwards them to GlitchTip.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const envelope = await request.text();
|
||||
|
||||
const realDsn = env.SENTRY_DSN;
|
||||
|
||||
if (!realDsn) {
|
||||
console.warn(
|
||||
"[Sentry Relay] Received payload but no SENTRY_DSN configured",
|
||||
);
|
||||
return NextResponse.json({ status: "ignored" }, { status: 200 });
|
||||
}
|
||||
|
||||
const dsnUrl = new URL(realDsn);
|
||||
const projectId = dsnUrl.pathname.replace("/", "");
|
||||
const relayUrl = `${dsnUrl.protocol}//${dsnUrl.host}/api/${projectId}/envelope/`;
|
||||
|
||||
const response = await fetch(relayUrl, {
|
||||
method: "POST",
|
||||
body: envelope,
|
||||
headers: {
|
||||
"Content-Type": "application/x-sentry-envelope",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("[Sentry Relay] GlitchTip API responded with error", {
|
||||
status: response.status,
|
||||
error: errorText.slice(0, 100),
|
||||
});
|
||||
return new NextResponse(errorText, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
} catch (error) {
|
||||
console.error("[Sentry Relay] Failed to relay Sentry request", {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
59
apps/web/app/(site)/global-error.tsx
Normal file
59
apps/web/app/(site)/global-error.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import Error from "next/error";
|
||||
import { useEffect } from "react";
|
||||
import { Inter, Newsreader } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-newsreader",
|
||||
style: "italic",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<body className="min-h-screen bg-white font-sans text-slate-900">
|
||||
<div className="flex flex-col items-center justify-center min-h-screen px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-red-50 border border-red-200 rounded text-[10px] font-mono text-red-600 uppercase tracking-widest border-dashed">
|
||||
Root Level Error
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-6xl font-black tracking-tighter text-slate-900">
|
||||
Systemausfall der Hauptebene.
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-slate-500 font-serif italic max-w-xl mx-auto">
|
||||
Ein kritischer Fehler auf der Root-Layout Ebene hat das Rendering
|
||||
blockiert. Der Vorfall wurde zur Untersuchung protokolliert.
|
||||
</p>
|
||||
|
||||
<div className="pt-8">
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="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 px-8 py-4 text-[10px] bg-slate-900 text-white hover:shadow-xl hover:-translate-y-0.5"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-3">
|
||||
Notfall-Neustart (Reset)
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
984
apps/web/app/(site)/globals.css
Normal file
984
apps/web/app/(site)/globals.css
Normal file
@@ -0,0 +1,984 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Base styles - Tailwind only */
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-sans font-bold text-slate-900 tracking-tighter;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl md:text-8xl leading-[1.1] md:leading-[0.95] mb-6 md:mb-12;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl md:text-6xl leading-tight mb-4 md:mb-8 mt-12 md:mt-16;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl md:text-5xl leading-tight mb-3 md:mb-6 mt-8 md:mt-12;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-lg md:text-3xl leading-tight mb-3 md:mb-4 mt-6 md:mt-8;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-6 text-base leading-relaxed text-slate-700;
|
||||
}
|
||||
|
||||
.lead {
|
||||
@apply text-base md:text-2xl text-slate-600 mb-6 leading-relaxed;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-slate-900 hover:text-slate-700 transition-colors no-underline;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@apply ml-5 mb-4;
|
||||
}
|
||||
|
||||
li {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-l-4 border-slate-900 pl-6 italic text-slate-700 my-8 text-lg md:text-2xl font-serif;
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
a:focus,
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Remove default tap highlight on mobile */
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Components - Tailwind utility classes */
|
||||
@layer components {
|
||||
/* Legacy hooks required by tests */
|
||||
.file-example {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-w-6xl mx-auto px-5 md:px-6 py-8 md:py-12;
|
||||
}
|
||||
|
||||
.wide-container {
|
||||
@apply max-w-7xl mx-auto px-5 md:px-6 py-10 md:py-16;
|
||||
}
|
||||
|
||||
.narrow-container {
|
||||
@apply max-w-4xl mx-auto px-5 md:px-6 py-6 md:py-10;
|
||||
}
|
||||
|
||||
.highlighter-tag {
|
||||
@apply inline-block text-[10px] uppercase tracking-wider font-bold px-3 py-1 rounded-full cursor-pointer transition-all duration-300;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
@apply w-full px-6 py-4 border border-slate-200 rounded-2xl focus:outline-none focus:border-slate-400 transition-all duration-300;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.search-box::placeholder {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
/* Blog post card */
|
||||
.post-card {
|
||||
@apply mb-8 last:mb-0;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
@apply text-xs text-slate-500 font-sans mb-2;
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
@apply text-slate-700 mb-2 leading-relaxed;
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
@apply flex flex-wrap gap-1;
|
||||
}
|
||||
|
||||
/* Article page */
|
||||
.article-header {
|
||||
@apply mb-12;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
@apply text-4xl md:text-5xl font-bold mb-3;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
@apply text-sm text-slate-500 font-sans mb-5;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
@apply font-serif antialiased text-slate-700;
|
||||
}
|
||||
|
||||
.article-content p {
|
||||
@apply text-lg md:text-xl leading-relaxed mb-6;
|
||||
}
|
||||
|
||||
.article-content h1 {
|
||||
@apply text-3xl md:text-5xl font-bold text-slate-900 mb-8 mt-12 tracking-tight leading-[1.1] font-sans;
|
||||
}
|
||||
|
||||
.article-content h2 {
|
||||
@apply text-2xl md:text-4xl font-bold text-slate-900 mb-6 mt-10 tracking-tight leading-tight font-sans;
|
||||
}
|
||||
|
||||
.article-content h3 {
|
||||
@apply text-xl md:text-2xl font-bold text-slate-900 mb-4 mt-8 tracking-tight leading-snug font-sans;
|
||||
}
|
||||
|
||||
.article-content ul,
|
||||
.article-content ol {
|
||||
@apply ml-6 mb-6 text-lg md:text-xl;
|
||||
}
|
||||
|
||||
.article-content li {
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
.article-content blockquote {
|
||||
@apply border-l-4 border-slate-900 pl-6 italic text-slate-700 my-10 text-xl md:text-2xl;
|
||||
}
|
||||
|
||||
/* 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 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 {
|
||||
@apply border-slate-900 text-slate-900 hover:bg-slate-900 hover:text-white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-slate-200 text-slate-500 hover:border-slate-400 hover:text-slate-900;
|
||||
}
|
||||
|
||||
/* Hide scrollbars but keep functionality */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
@apply text-center py-8 text-slate-500;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
@apply mx-auto mb-2 text-slate-300;
|
||||
}
|
||||
|
||||
/* Line clamp utility */
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Reading progress indicator */
|
||||
.reading-progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #0f172a;
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
z-index: 100;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* Floating back to top button */
|
||||
.floating-back-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.floating-back-to-top.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.floating-back-to-top:hover {
|
||||
background: #f8fafc;
|
||||
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);
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.floating-back-to-top,
|
||||
.reading-progress-bar {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional global styles from BaseLayout */
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Tag Styles */
|
||||
.highlighter-tag {
|
||||
animation: tagPopIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
animation-delay: calc(var(--tag-index, 0) * 0.05s);
|
||||
}
|
||||
|
||||
.highlighter-tag:hover {
|
||||
@apply -translate-y-0.5 scale-105 shadow-lg shadow-slate-200;
|
||||
}
|
||||
|
||||
@keyframes tagPopIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.highlighter-yellow {
|
||||
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%
|
||||
);
|
||||
color: #3f0018;
|
||||
}
|
||||
|
||||
.highlighter-green {
|
||||
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%
|
||||
);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.highlighter-tag:hover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: inherit;
|
||||
filter: blur(8px);
|
||||
opacity: 0.4;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.highlighter-tag:active {
|
||||
transform: rotate(-1deg) translateY(0) scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.highlighter-tag:focus {
|
||||
@apply -translate-y-0.5 scale-105;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Marker Title Styles */
|
||||
.marker-title::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -0.15em;
|
||||
right: -0.15em;
|
||||
bottom: 0.05em;
|
||||
height: 0.62em;
|
||||
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.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));
|
||||
|
||||
filter: saturate(1.05);
|
||||
}
|
||||
|
||||
.marker-title::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -0.18em;
|
||||
right: -0.05em;
|
||||
bottom: 0.05em;
|
||||
height: 0.62em;
|
||||
border-radius: 0.18em;
|
||||
z-index: -1;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mermaid Styles */
|
||||
.mermaid-container svg {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.mermaid-container rect,
|
||||
.mermaid-container circle,
|
||||
.mermaid-container ellipse,
|
||||
.mermaid-container polygon,
|
||||
.mermaid-container path,
|
||||
.mermaid-container .actor,
|
||||
.mermaid-container .node {
|
||||
fill: white !important;
|
||||
stroke: #cbd5e1 !important;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
|
||||
.mermaid-container .edgePath .path,
|
||||
.mermaid-container .messageLine0,
|
||||
.mermaid-container .messageLine1,
|
||||
.mermaid-container .flowchart-link {
|
||||
stroke: #cbd5e1 !important;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
|
||||
.mermaid-container text,
|
||||
.mermaid-container .label,
|
||||
.mermaid-container .labelText,
|
||||
.mermaid-container .edgeLabel,
|
||||
.mermaid-container .node text,
|
||||
.mermaid-container tspan {
|
||||
font-family: "Inter", sans-serif !important;
|
||||
fill: #334155 !important;
|
||||
color: #334155 !important;
|
||||
stroke: none !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.mermaid-container .marker,
|
||||
.mermaid-container marker path {
|
||||
fill: #cbd5e1 !important;
|
||||
stroke: #cbd5e1 !important;
|
||||
}
|
||||
|
||||
/* Generic Embed Styles */
|
||||
.generic-embed {
|
||||
--max-width: 100%;
|
||||
--border-radius: 24px;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow: none;
|
||||
|
||||
margin: 1.5rem 0;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.embed-wrapper {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.generic-embed[data-type="video"] .embed-wrapper {
|
||||
aspect-ratio: 16/9;
|
||||
height: 0;
|
||||
padding-bottom: 56.25%;
|
||||
/* 16:9 */
|
||||
}
|
||||
|
||||
.generic-embed[data-type="video"] .embed-wrapper iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.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);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="youtube.com"] {
|
||||
--bg-color: #000000;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="vimeo.com"] {
|
||||
--bg-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.generic-embed[data-provider="codepen.io"] {
|
||||
--bg-color: #1e1e1e;
|
||||
--border-color: #333;
|
||||
}
|
||||
|
||||
.embed-fallback {
|
||||
padding: 1.5rem;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fallback-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.fallback-link {
|
||||
@apply text-slate-900 underline underline-offset-4;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.fallback-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.generic-embed {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.embed-fallback {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.embed-wrapper:hover {
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* File Example Styles */
|
||||
[data-file-example] {
|
||||
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.copy-btn,
|
||||
.download-btn {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.copy-btn[data-copied="true"] {
|
||||
color: #065f46;
|
||||
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-"]) {
|
||||
color: #0f172a;
|
||||
background: transparent;
|
||||
text-shadow: none;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.65;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
tab-size: 2;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number {
|
||||
color: #a16207;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #7c3aed;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
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;
|
||||
}
|
||||
}
|
||||
46
apps/web/app/(site)/layout.tsx
Normal file
46
apps/web/app/(site)/layout.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Newsreader } from "next/font/google";
|
||||
import { Analytics } from "@/src/components/Analytics";
|
||||
import { Footer } from "@/src/components/Footer";
|
||||
import { Header } from "@/src/components/Header";
|
||||
import { InteractiveElements } from "@/src/components/InteractiveElements";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
const newsreader = Newsreader({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-newsreader",
|
||||
style: "italic",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "Marc Mintel",
|
||||
template: "%s | Marc Mintel",
|
||||
},
|
||||
description:
|
||||
"Technical problem solver's blog - practical insights and learning notes",
|
||||
metadataBase: new URL("https://mintel.me"),
|
||||
icons: {
|
||||
icon: "/favicon.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${newsreader.variable}`}>
|
||||
<body className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
<InteractiveElements />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
52
apps/web/app/(site)/not-found.tsx
Normal file
52
apps/web/app/(site)/not-found.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
export const metadata = {
|
||||
title: "404 - Seite nicht gefunden | Marc Mintel",
|
||||
description: "Diese Seite konnte leider nicht gefunden werden.",
|
||||
};
|
||||
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/src/components/Button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] px-5 py-20 text-center">
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<span className="inline-block px-3 py-1 bg-slate-50 border border-slate-100 rounded text-[10px] font-mono text-slate-500 uppercase tracking-widest">
|
||||
Error 404
|
||||
</span>
|
||||
<h1 className="text-5xl md:text-7xl font-black text-slate-900 tracking-tighter">
|
||||
System-Anomalie.
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-slate-500 font-serif italic max-w-xl mx-auto leading-relaxed">
|
||||
Die angeforderte URL existiert nicht in dieser Zeitleiste.
|
||||
Möglicherweise wurde die Seite verschoben oder gelöscht.
|
||||
</p>
|
||||
|
||||
<div className="pt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button href="/" variant="primary">
|
||||
Zurück zur Basis
|
||||
</Button>
|
||||
<Button href="/blog" variant="outline">
|
||||
Aktuelle Artikel lesen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-16 max-w-sm mx-auto">
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100 text-left font-mono text-xs text-slate-400">
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>STATUS</span>
|
||||
<span className="text-red-500">404 NOT_FOUND</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-200/60 pb-2 mb-2">
|
||||
<span>ACTION</span>
|
||||
<span>REROUTE_SUGGESTED</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>SYSTEM</span>
|
||||
<span className="text-green-500">ONLINE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
apps/web/app/(site)/opengraph-image.tsx
Normal file
23
apps/web/app/(site)/opengraph-image.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { OGImageTemplate } from "../../src/components/OGImageTemplate";
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from "../../src/lib/og-helper";
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = "image/png";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export default async function Image() {
|
||||
const fonts = await getOgFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
title="Marc Mintel"
|
||||
description="Technical problem solver's blog - practical insights and learning notes"
|
||||
label="Engineering"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts: fonts as any,
|
||||
},
|
||||
);
|
||||
}
|
||||
327
apps/web/app/(site)/page.tsx
Normal file
327
apps/web/app/(site)/page.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ComparisonRow,
|
||||
ConceptCode,
|
||||
ConceptCommunication,
|
||||
ConceptPrice,
|
||||
ConceptPrototyping,
|
||||
ConceptWebsite,
|
||||
} from "@/src/components/Landing";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import {
|
||||
H1,
|
||||
H3,
|
||||
LeadText,
|
||||
BodyText,
|
||||
MonoLabel,
|
||||
Label,
|
||||
} from "@/src/components/Typography";
|
||||
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";
|
||||
import { PenCircle } from "@/src/components/PenCircle";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
{/* Dark Hero */}
|
||||
<HeroSection />
|
||||
|
||||
{/* Rest of page on white */}
|
||||
|
||||
{/* Section 02: The Promise – Streamlined */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Das Versprechen"
|
||||
borderTop
|
||||
effects={<GradientMesh variant="metallic" className="opacity-70" />}
|
||||
>
|
||||
<div className="space-y-10 md:space-y-16 relative">
|
||||
<Reveal>
|
||||
<H3 className="max-w-3xl">
|
||||
Kein Agentur-Zirkus. <br />
|
||||
<Marker delay={0.3}>Ergebnisse.</Marker>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
|
||||
{[
|
||||
{
|
||||
icon: <ConceptCommunication className="w-8 h-8" />,
|
||||
title: "Direkte Kommunikation",
|
||||
text: "Sie sprechen mit dem Entwickler. Keine Stille Post, keine Umwege.",
|
||||
},
|
||||
{
|
||||
icon: <ConceptPrototyping className="w-8 h-8" />,
|
||||
title: "Schnelle Umsetzung",
|
||||
text: "Sichtbare Fortschritte in Tagen. Prototypen statt Konzeptpapiere.",
|
||||
},
|
||||
{
|
||||
icon: <ConceptCode className="w-8 h-8" />,
|
||||
title: "Sauberer Code",
|
||||
text: "Maßgeschneiderte Architektur. Kein Baukasten, kein Plugin-Chaos.",
|
||||
},
|
||||
{
|
||||
icon: <ConceptPrice className="w-8 h-8" />,
|
||||
title: "Klare Fixpreise",
|
||||
text: "Volle Budgetsicherheit. Keine versteckten Kosten.",
|
||||
},
|
||||
].map((item, i) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.1}>
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="normal"
|
||||
techBorder
|
||||
className="group"
|
||||
>
|
||||
<div className="space-y-4 relative z-10">
|
||||
<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}
|
||||
</div>
|
||||
<Label className="text-slate-900">{item.title}</Label>
|
||||
<BodyText className="text-slate-500">{item.text}</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 03: The Difference – Visual Comparison */}
|
||||
<Section number="03" title="Der Unterschied" variant="white" borderTop>
|
||||
<div className="space-y-10 md:space-y-16 relative">
|
||||
<Reveal>
|
||||
<H3 className="max-w-3xl">
|
||||
Ich arbeite für das Ergebnis, <br />
|
||||
nicht gegen die <Marker delay={0.4}>Uhr.</Marker>
|
||||
</H3>
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 relative z-20">
|
||||
<ComparisonRow
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Wochen in Planung, bevor eine einzige Zeile Code geschrieben wird."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText={
|
||||
<>
|
||||
Schnelle Prototypen. Ergebnisse in{" "}
|
||||
<PenCircle delay={0.5}>Tagen</PenCircle>, nicht Monaten.
|
||||
</>
|
||||
}
|
||||
delay={0.1}
|
||||
/>
|
||||
<ComparisonRow
|
||||
negativeLabel="Klassisch"
|
||||
negativeText="Unvorhersehbare Kosten durch Stundenabrechnungen."
|
||||
positiveLabel="Mein Weg"
|
||||
positiveText={
|
||||
<>
|
||||
<PenCircle delay={0.5}>Fixpreise.</PenCircle> Sie wissen von
|
||||
Anfang an, was es kostet.
|
||||
</>
|
||||
}
|
||||
reverse
|
||||
delay={0.2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 04: Target Group */}
|
||||
<Section number="04" title="Für wen" borderTop>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 relative z-10">
|
||||
<Reveal>
|
||||
<Card variant="glass" padding="normal" techBorder className="group">
|
||||
<div className="space-y-4 md:space-y-6 relative overflow-hidden">
|
||||
<div className="w-12 h-12 md:w-16 md:h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
|
||||
<ConceptPrice className="w-6 h-6 md:w-8 md:h-8" />
|
||||
</div>
|
||||
<H3 className="text-xl md:text-3xl">
|
||||
Unternehmer & <br />
|
||||
Geschäftsführer
|
||||
</H3>
|
||||
<LeadText className="text-slate-400 text-base md:text-lg">
|
||||
Sie wollen eine Website, die funktioniert – ohne sich mit
|
||||
Technik beschäftigen zu müssen.
|
||||
</LeadText>
|
||||
</div>
|
||||
<div className="pt-6 md:pt-8 border-t border-slate-50 mt-6 md:mt-8">
|
||||
<Label className="group-hover:text-slate-900 transition-colors">
|
||||
Perfekt für Sie
|
||||
</Label>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<Card variant="glass" padding="normal" techBorder className="group">
|
||||
<div className="space-y-6 relative overflow-hidden">
|
||||
<div className="w-12 h-12 md:w-16 md:h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
|
||||
{/* Icon placeholder or same as above if needed */}
|
||||
<ConceptWebsite className="w-6 h-6 md:w-8 md:h-8" />
|
||||
</div>
|
||||
<H3 className="text-xl md:text-3xl">
|
||||
Marketing & <br />
|
||||
Vertrieb
|
||||
</H3>
|
||||
<LeadText className="text-slate-400 text-base md:text-lg">
|
||||
Sie brauchen Landingpages und Tools, die Ergebnisse liefern.
|
||||
Schnell und zuverlässig.
|
||||
</LeadText>
|
||||
</div>
|
||||
<div className="pt-6 md:pt-8 border-t border-slate-50 mt-6 md:mt-8">
|
||||
<Label className="group-hover:text-slate-900 transition-colors">
|
||||
Perfekt für Sie
|
||||
</Label>
|
||||
</div>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 05: Leistungen — Interactive Service Rows */}
|
||||
<Section number="05" title="Leistungen" variant="gray" borderTop>
|
||||
<div className="space-y-0 relative z-20">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.15}>
|
||||
<div className="group py-8 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-6 md:gap-16">
|
||||
{/* Number + Binary */}
|
||||
<div className="shrink-0 flex md:block items-baseline gap-4">
|
||||
<span className="text-4xl 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 leading-none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{service.binary}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 space-y-4 md:space-y-6">
|
||||
<H3 className="text-xl 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 text-sm md:text-base 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-[8px] md: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 pt-4 md:pt-0">
|
||||
<Button
|
||||
href={service.href}
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
showArrow
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section 06: Contact */}
|
||||
<Section number="06" title="Kontakt" borderTop>
|
||||
<div className="relative py-4 md:py-12" id="contact">
|
||||
<Reveal>
|
||||
<div className="space-y-8 md:space-y-16">
|
||||
<H1 className="text-3xl md:text-8xl">
|
||||
Lassen Sie uns <br />
|
||||
<span className="text-slate-400">starten.</span>
|
||||
</H1>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 md:gap-16 items-start relative z-10">
|
||||
<div className="space-y-4 md:space-y-8 flex-1">
|
||||
<LeadText className="text-lg md:text-3xl text-slate-400">
|
||||
Beschreiben Sie kurz Ihr Vorhaben. Ich melde mich{" "}
|
||||
<span className="text-slate-900 border-b-2 border-slate-900/10">
|
||||
<Marker>zeitnah</Marker>
|
||||
</span>{" "}
|
||||
bei Ihnen.
|
||||
</LeadText>
|
||||
<div className="pt-2 md:pt-4">
|
||||
<Button
|
||||
href="/contact"
|
||||
size="large"
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-72 space-y-4 md:space-y-6 p-6 glass rounded-2xl border border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<Label className="text-slate-900">Verfügbarkeit</Label>
|
||||
</div>
|
||||
<BodyText className="text-sm md:text-base leading-snug">
|
||||
Aktuell nehme ich Projekte für{" "}
|
||||
<span className="font-bold text-slate-900">Q2 2026</span>{" "}
|
||||
an.
|
||||
</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
apps/web/app/(site)/sitemap.ts
Normal file
57
apps/web/app/(site)/sitemap.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { technologies } from "./technologies/[slug]/data";
|
||||
|
||||
/**
|
||||
* Sitemap Generator
|
||||
*
|
||||
* Standard Next.js 15 App Router sitemap generation.
|
||||
* This file dynamically generates /sitemap.xml
|
||||
*/
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const allPosts = await getAllPosts();
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://mintel.me";
|
||||
|
||||
// 1. Core Pages
|
||||
const routes = [
|
||||
"",
|
||||
"/about",
|
||||
"/blog",
|
||||
"/case-studies",
|
||||
"/case-studies/klz-cables",
|
||||
"/contact",
|
||||
"/websites",
|
||||
].map((route) => ({
|
||||
url: `${baseUrl}${route}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: route === "" ? 1.0 : 0.8,
|
||||
}));
|
||||
|
||||
// 2. Dynamic Blog Posts
|
||||
const blogRoutes = allPosts.map((post) => ({
|
||||
url: `${baseUrl}/blog/${post.slug}`,
|
||||
lastModified: new Date(post.date),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
// 3. Technology Detail Pages
|
||||
const techRoutes = Object.keys(technologies).map((slug) => ({
|
||||
url: `${baseUrl}/technologies/${slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
// 4. Tag Pages
|
||||
const allTags = [...new Set(allPosts.flatMap((post) => post.tags))];
|
||||
const tagRoutes = allTags.map((tag) => ({
|
||||
url: `${baseUrl}/tags/${tag}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.3,
|
||||
}));
|
||||
|
||||
return [...routes, ...blogRoutes, ...techRoutes, ...tagRoutes];
|
||||
}
|
||||
77
apps/web/app/(site)/stats/api/send/route.ts
Normal file
77
apps/web/app/(site)/stats/api/send/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* Smart Proxy for Umami Analytics.
|
||||
*
|
||||
* This Route Handler receives tracking events from the browser,
|
||||
* injects the secret UMAMI_WEBSITE_ID, and forwards them to the
|
||||
* internal Umami API endpoint.
|
||||
*
|
||||
* This ensures:
|
||||
* 1. The Website ID is NOT leaked to the client bundle.
|
||||
* 2. The Umami API endpoint is hidden behind our domain.
|
||||
* 3. We have full control over the tracking data.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { type, payload } = body;
|
||||
|
||||
// Inject the secret websiteId from server config
|
||||
const websiteId = env.UMAMI_WEBSITE_ID || env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
||||
|
||||
if (!websiteId) {
|
||||
console.warn(
|
||||
"Umami tracking received but no Website ID configured on server",
|
||||
);
|
||||
return NextResponse.json({ status: "ignored" }, { status: 200 });
|
||||
}
|
||||
|
||||
// Prepare the enhanced payload with the secret ID
|
||||
const enhancedPayload = {
|
||||
...payload,
|
||||
website: websiteId,
|
||||
};
|
||||
|
||||
const umamiEndpoint = env.UMAMI_API_ENDPOINT;
|
||||
|
||||
// Log the event (debug only)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Forwarding analytics event", {
|
||||
type,
|
||||
url: payload.url,
|
||||
website: websiteId.slice(0, 8) + "...",
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`${umamiEndpoint}/api/send`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": request.headers.get("user-agent") || "Mintel-Smart-Proxy",
|
||||
"X-Forwarded-For": request.headers.get("x-forwarded-for") || "",
|
||||
},
|
||||
body: JSON.stringify({ type, payload: enhancedPayload }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Umami API responded with error", {
|
||||
status: response.status,
|
||||
error: errorText.slice(0, 100),
|
||||
});
|
||||
return new NextResponse(errorText, { status: response.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
} catch (error) {
|
||||
console.error("Failed to proxy analytics request", {
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
61
apps/web/app/(site)/tags/[tag]/page.tsx
Normal file
61
apps/web/app/(site)/tags/[tag]/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import Link from "next/link";
|
||||
import { getAllPosts } from "@/src/lib/posts";
|
||||
import { MediumCard } from "@/src/components/MediumCard";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allPosts = await getAllPosts();
|
||||
const allTags = Array.from(
|
||||
new Set(allPosts.flatMap((post) => post.tags || [])),
|
||||
);
|
||||
return allTags.map((tag) => ({
|
||||
tag,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function TagPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tag: string }>;
|
||||
}) {
|
||||
const { tag } = await params;
|
||||
const allPosts = await getAllPosts();
|
||||
const posts = allPosts.filter((post) => post.tags?.includes(tag));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<Reveal>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-2">
|
||||
Posts tagged{" "}
|
||||
<span className="highlighter-yellow px-2 rounded">{tag}</span>
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.1}>
|
||||
<p className="text-slate-600">
|
||||
{posts.length} post{posts.length === 1 ? "" : "s"}
|
||||
</p>
|
||||
</Reveal>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map((post, i) => (
|
||||
<Reveal key={post.slug} delay={0.1 + i * 0.05} width="100%">
|
||||
<MediumCard post={post} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.3}>
|
||||
<div className="mt-8 pt-6 border-t border-slate-200">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="text-slate-600 hover:text-slate-900 inline-flex items-center"
|
||||
>
|
||||
← Back to blog
|
||||
</Link>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
apps/web/app/(site)/technologies/[slug]/content.tsx
Normal file
141
apps/web/app/(site)/technologies/[slug]/content.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { Container } from "@/src/components/Layout";
|
||||
import { Label } from "@/src/components/Typography";
|
||||
import { Check, ArrowLeft, Zap, ExternalLink } from "lucide-react";
|
||||
import { technologies } from "./data";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
|
||||
export default function TechnologyContent({ slug }: { slug: string }) {
|
||||
const tech = technologies[slug];
|
||||
|
||||
if (!tech) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Technology Not Found</h1>
|
||||
<Link href="/" className="text-blue-600 hover:underline">
|
||||
Return Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = tech.icon;
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen text-slate-900 pb-24">
|
||||
<div className="bg-slate-50 border-b border-slate-200">
|
||||
<Container className="py-24">
|
||||
<Reveal>
|
||||
<Link
|
||||
href="/case-studies/klz-cables"
|
||||
className="inline-flex items-center text-sm font-bold text-slate-500 hover:text-slate-900 mb-8 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Case Study
|
||||
</Link>
|
||||
</Reveal>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-start gap-8">
|
||||
<Reveal>
|
||||
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
|
||||
<Icon className="w-12 h-12" />
|
||||
</div>
|
||||
</Reveal>
|
||||
<div className="flex-1">
|
||||
<Reveal delay={0.1}>
|
||||
<Label className="text-slate-400 mb-2">
|
||||
TECHNOLOGY DEEP DIVE
|
||||
</Label>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||
{tech.title}
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.3}>
|
||||
<p className="text-xl text-slate-500 font-medium">
|
||||
{tech.subtitle}
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Container className="py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16">
|
||||
<div className="lg:col-span-8 space-y-12">
|
||||
<section>
|
||||
<Reveal>
|
||||
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
|
||||
<p className="text-xl leading-relaxed text-slate-700">
|
||||
{tech.description}
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<Reveal>
|
||||
<h2 className="text-2xl font-bold mb-6 flex items-center gap-3">
|
||||
<Zap className="w-6 h-6 text-amber-500" /> Why I use it
|
||||
</h2>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{tech.benefits.map((benefit, i) => (
|
||||
<Reveal key={i} delay={0.1 + i * 0.05}>
|
||||
<div className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100 h-full">
|
||||
<Check className="w-5 h-5 text-green-600 shrink-0" />
|
||||
<span className="font-medium text-slate-700">
|
||||
{benefit}
|
||||
</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Reveal delay={0.4}>
|
||||
<section className="bg-blue-50 border border-blue-100 rounded-2xl p-8">
|
||||
<Label className="text-blue-600 mb-2">CUSTOMER IMPACT</Label>
|
||||
<h3 className="text-2xl font-bold text-slate-900 mb-4">
|
||||
What does this mean for you?
|
||||
</h3>
|
||||
<p className="text-lg text-slate-700 leading-relaxed">
|
||||
{tech.customerValue}
|
||||
</p>
|
||||
</section>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<Reveal delay={0.5}>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 sticky top-24">
|
||||
<h3 className="font-bold text-slate-900 mb-4">
|
||||
Related Technologies
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{tech.related.map((item) => (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={`/technologies/${item.slug}`}
|
||||
className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-slate-400 hover:shadow-sm transition-all group"
|
||||
>
|
||||
<span className="font-medium text-slate-700 group-hover:text-slate-900">
|
||||
{item.name}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-slate-400 group-hover:text-slate-600" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
apps/web/app/(site)/technologies/[slug]/data.tsx
Normal file
115
apps/web/app/(site)/technologies/[slug]/data.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Layers, Code, Database, Palette, Terminal } from "lucide-react";
|
||||
|
||||
export interface TechInfo {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
icon: any; // React.ElementType
|
||||
benefits: string[];
|
||||
customerValue: string;
|
||||
color: string;
|
||||
related: { name: string; slug: string }[];
|
||||
}
|
||||
|
||||
export const technologies: Record<string, TechInfo> = {
|
||||
"next-js-14": {
|
||||
title: "Next.js 14",
|
||||
subtitle: "The React Framework for the Web",
|
||||
description:
|
||||
"Next.js 14 is the latest version of the industry-leading framework for building high-performance web applications. It allows me to create fast, scalable, and search-engine-friendly websites by rendering content on the server before sending it to your users.",
|
||||
icon: Layers,
|
||||
benefits: [
|
||||
"Lightning-fast page loads with Server Components",
|
||||
"Automatic image optimization",
|
||||
"Instant navigation between pages",
|
||||
"Top-tier SEO (Search Engine Optimization) out of the box",
|
||||
],
|
||||
customerValue:
|
||||
"For my clients, Next.js means a website that ranks higher on Google, keeps visitors engaged with instant interactions, and scales effortlessly as your traffic grows.",
|
||||
color: "bg-black text-white",
|
||||
related: [
|
||||
{ name: "TypeScript", slug: "typescript" },
|
||||
{ name: "React", slug: "react" },
|
||||
],
|
||||
},
|
||||
typescript: {
|
||||
title: "TypeScript",
|
||||
subtitle: "JavaScript with Syntax for Types",
|
||||
description:
|
||||
"TypeScript adds a powerful type system to JavaScript, catching errors before they ever reach production. It acts as a safety net for your code, ensuring that data flows exactly as expected through your entire application.",
|
||||
icon: Code,
|
||||
benefits: [
|
||||
"Eliminates whole categories of common bugs",
|
||||
"Makes large codebases easier to maintain",
|
||||
"Improves developer productivity and code confidence",
|
||||
"Ensures critical data integrity",
|
||||
],
|
||||
customerValue:
|
||||
'Using TypeScript means your application is robust and reliable from day one. It dramatically reduces the risk of "runtime errors" that could crash your site, saving time and money on bug fixes down the line.',
|
||||
color: "bg-blue-600 text-white",
|
||||
related: [
|
||||
{ name: "Directus CMS", slug: "directus-cms" },
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
"directus-cms": {
|
||||
title: "Directus CMS",
|
||||
subtitle: "The Open Data Platform",
|
||||
description:
|
||||
"Directus is a modern, headless Content Management System (CMS) that instantly turns any database into a beautiful, easy-to-use application for managing your content. Unlike traditional CMSs, it doesn't dictate how your website looks.",
|
||||
icon: Database,
|
||||
benefits: [
|
||||
"Intuitive interface for non-technical editors",
|
||||
"Complete freedom regarding front-end design",
|
||||
"Real-time updates and live previews",
|
||||
"Highly secure and role-based access control",
|
||||
],
|
||||
customerValue:
|
||||
"Directus gives you full control over your content without needing a developer for every text change. It separates your data from the design, ensuring your website can evolve visually without rebuilding your entire content library.",
|
||||
color: "bg-purple-600 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
||||
],
|
||||
},
|
||||
"tailwind-css": {
|
||||
title: "Tailwind CSS",
|
||||
subtitle: "Utility-First CSS Framework",
|
||||
description:
|
||||
"Tailwind CSS is a utility-first framework that allows me to build custom designs directly in markup. It eliminates the need for bulky, overriding stylesheets and ensures a consistent design system across every page.",
|
||||
icon: Palette,
|
||||
benefits: [
|
||||
"Rapid UI development and prototyping",
|
||||
"Consistent spacing, colors, and typography",
|
||||
"Highly optimized final bundle size (only includes used styles)",
|
||||
"Responsive design made simple",
|
||||
],
|
||||
customerValue:
|
||||
"Tailwind ensures your brand looks pixel-perfect on every device. It also results in incredibly small CSS files, meaning your site loads faster for users on mobile networks.",
|
||||
color: "bg-cyan-500 text-white",
|
||||
related: [
|
||||
{ name: "React", slug: "react" },
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
],
|
||||
},
|
||||
react: {
|
||||
title: "React",
|
||||
subtitle: "The Library for Web and Native User Interfaces",
|
||||
description:
|
||||
"React is the core library powering Next.js. It lets me build encapsulated components that manage their own state, then compose them to make complex UIs.",
|
||||
icon: Terminal,
|
||||
benefits: [
|
||||
"Component-based architecture for reuse",
|
||||
"Efficient updates and rendering",
|
||||
"Rich ecosystem of libraries and tools",
|
||||
"Strong community support",
|
||||
],
|
||||
customerValue:
|
||||
"React enables rich, app-like interactions on your website. Whether it's a complex dashboard or a smooth animation, React makes it possible to build dynamic experiences that feel instantaneous.",
|
||||
color: "bg-blue-400 text-white",
|
||||
related: [
|
||||
{ name: "Next.js 14", slug: "next-js-14" },
|
||||
{ name: "Tailwind CSS", slug: "tailwind-css" },
|
||||
],
|
||||
},
|
||||
};
|
||||
19
apps/web/app/(site)/technologies/[slug]/page.tsx
Normal file
19
apps/web/app/(site)/technologies/[slug]/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { technologies } from "./data";
|
||||
import TechnologyContent from "./content";
|
||||
|
||||
export default async function TechnologyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
return <TechnologyContent slug={slug} />;
|
||||
}
|
||||
|
||||
// Generate static params for these dynamic routes
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(technologies).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
375
apps/web/app/(site)/websites/page.tsx
Normal file
375
apps/web/app/(site)/websites/page.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
"use client";
|
||||
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { Section } from "@/src/components/Section";
|
||||
import {
|
||||
SpeedPerformance,
|
||||
SolidFoundation,
|
||||
LayerSeparation,
|
||||
TaskDone,
|
||||
} from "@/src/components/Landing";
|
||||
import {
|
||||
H3,
|
||||
LeadText,
|
||||
BodyText,
|
||||
Label,
|
||||
MonoLabel,
|
||||
} from "@/src/components/Typography";
|
||||
import { Card } from "@/src/components/Layout";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { IconList, IconListItem } from "@/src/components/IconList";
|
||||
import {
|
||||
GradientMesh,
|
||||
CodeSnippet,
|
||||
AbstractCircuit,
|
||||
CMSVisualizer,
|
||||
ArchitectureVisualizer,
|
||||
ResultVisualizer,
|
||||
} from "@/src/components/Effects";
|
||||
import { Marker } from "@/src/components/Marker";
|
||||
|
||||
export default function WebsitesPage() {
|
||||
return (
|
||||
<div className="flex flex-col bg-white overflow-hidden relative">
|
||||
<AbstractCircuit />
|
||||
|
||||
<Section className="pt-24 pb-12 md:pt-40 md:pb-24">
|
||||
<div className="space-y-12 md:space-y-24">
|
||||
<div className="space-y-6 md:space-y-10 max-w-5xl">
|
||||
<Reveal>
|
||||
<div className="space-y-4">
|
||||
<MonoLabel className="text-blue-500 tracking-[0.2em] text-[10px] md:text-xs">
|
||||
SYSTEM ENGINEERING
|
||||
</MonoLabel>
|
||||
<H3 className="text-4xl md:text-8xl leading-[1.0] tracking-tighter">
|
||||
Websites, die einfach <br />
|
||||
<span className="text-slate-400">
|
||||
<Marker>funktionieren.</Marker>
|
||||
</span>
|
||||
</H3>
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-lg md:text-2xl max-w-2xl text-slate-500 md:text-slate-400 leading-relaxed">
|
||||
Kein Baukasten. Kein Plugin-Chaos. Maßgeschneiderte Architektur
|
||||
für{" "}
|
||||
<span className="text-slate-900 font-bold underline decoration-slate-200 underline-offset-8">
|
||||
maximale Performance
|
||||
</span>
|
||||
.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="space-y-12">
|
||||
<Reveal delay={0.3} direction="up">
|
||||
<ArchitectureVisualizer />
|
||||
</Reveal>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{
|
||||
label: "Next.js",
|
||||
sub: "Architecture",
|
||||
desc: "React-Framework für maximale SEO & Speed.",
|
||||
},
|
||||
{
|
||||
label: "Docker",
|
||||
sub: "Infrastructure",
|
||||
desc: "Reproduzierbare Umgebungen überall.",
|
||||
},
|
||||
{
|
||||
label: "Directus",
|
||||
sub: "Management",
|
||||
desc: "Headless CMS für flexible Datenabfrage.",
|
||||
},
|
||||
{
|
||||
label: "Gitea",
|
||||
sub: "Pipeline",
|
||||
desc: "Self-hosted Git & CI/CD Pipelines.",
|
||||
},
|
||||
].map((item, i) => (
|
||||
<Reveal key={i} delay={0.4 + i * 0.1}>
|
||||
<div className="space-y-2 p-6 rounded-2xl border border-slate-50 bg-white shadow-sm hover:border-slate-200 transition-all group">
|
||||
<Label className="text-slate-900 group-hover:text-blue-600 transition-colors uppercase tracking-widest text-[10px]">
|
||||
{item.label}
|
||||
</Label>
|
||||
<BodyText className="text-xs text-slate-400">
|
||||
{item.desc}
|
||||
</BodyText>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 02: Performance */}
|
||||
<Section
|
||||
number="02"
|
||||
title="Performance"
|
||||
borderTop
|
||||
variant="gray"
|
||||
illustration={<SpeedPerformance className="w-24 h-24" />}
|
||||
effects={<GradientMesh variant="metallic" className="opacity-60" />}
|
||||
>
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Geschwindigkeit ist <br />
|
||||
<span className="text-slate-400">
|
||||
kein Extra. Sie ist <Marker delay={0.3}>Standard.</Marker>
|
||||
</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-center">
|
||||
<div className="md:col-span-12 lg:col-span-7 space-y-6 md:space-y-8">
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-lg md:text-xl text-slate-500 md:text-slate-400">
|
||||
Jede Seite wird vorab gerendert und über ein CDN ausgeliefert.
|
||||
Das Ergebnis: Ladezeiten unter einer Sekunde. Messbar.{" "}
|
||||
<span className="text-slate-900">Reproduzierbar.</span>
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
<Reveal delay={0.4}>
|
||||
<IconList className="space-y-2 md:space-y-4">
|
||||
{[
|
||||
"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) => (
|
||||
<IconListItem key={i} bullet>
|
||||
<LeadText className="text-base md:text-xl">
|
||||
{item}
|
||||
</LeadText>
|
||||
</IconListItem>
|
||||
))}
|
||||
</IconList>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="md:col-span-5">
|
||||
<Reveal delay={0.6}>
|
||||
<Card
|
||||
variant="glass"
|
||||
padding="normal"
|
||||
techBorder
|
||||
className="text-center group py-10 md:py-12"
|
||||
>
|
||||
<div className="text-5xl md:text-8xl font-bold text-slate-900 tracking-tighter group-hover:scale-110 transition-transform duration-700">
|
||||
90+
|
||||
</div>
|
||||
<Label className="mt-4">Lighthouse Score</Label>
|
||||
<span className="block text-[8px] md:text-[9px] font-mono text-slate-300 mt-2 tracking-wider">
|
||||
PERFORMANCE · ACCESSIBILITY · SEO
|
||||
</span>
|
||||
</Card>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 03: Code-Qualität */}
|
||||
<Section
|
||||
number="03"
|
||||
title="Code"
|
||||
borderTop
|
||||
illustration={<SolidFoundation className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<Reveal>
|
||||
<H3 className="text-2xl md:text-5xl leading-tight max-w-3xl">
|
||||
Keine Plugins. <br />
|
||||
<span className="text-slate-400">Keine Abhängigkeiten.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-500 md:text-slate-400">
|
||||
Ihre Website besteht aus{" "}
|
||||
<span className="text-slate-900">Ihrem Code</span>. Kein
|
||||
WordPress, kein Wix, keine Blackbox. Alles versioniert, alles
|
||||
nachvollziehbar.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
|
||||
{/* Git Branch Visualization */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<Reveal delay={0.4}>
|
||||
<CodeSnippet variant="git" />
|
||||
</Reveal>
|
||||
<Reveal delay={0.5}>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<Card variant="glass" padding="normal" className="group">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-900">Versionskontrolle</Label>
|
||||
<BodyText>
|
||||
Jede Änderung ist dokumentiert. Rollbacks in Sekunden.
|
||||
Kein „wer hat das kaputt gemacht?".
|
||||
</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="glass" padding="normal" className="group">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-900">
|
||||
Automatisches Deployment
|
||||
</Label>
|
||||
<BodyText>
|
||||
Code wird geprüft, getestet und automatisch live
|
||||
geschaltet. Ohne manuellen Eingriff.
|
||||
</BodyText>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 04: Content-System */}
|
||||
<Section
|
||||
number="04"
|
||||
title="Inhalte"
|
||||
borderTop
|
||||
variant="gray"
|
||||
illustration={<LayerSeparation className="w-24 h-24" />}
|
||||
effects={<GradientMesh variant="subtle" className="opacity-60" />}
|
||||
>
|
||||
<div className="space-y-12 md:space-y-20">
|
||||
<div className="space-y-6 md:space-y-10 max-w-5xl">
|
||||
<Reveal>
|
||||
<div className="space-y-4">
|
||||
<MonoLabel className="text-blue-500 tracking-[0.2em] text-[10px] md:text-xs">
|
||||
ARCHITECTURAL SEPARATION
|
||||
</MonoLabel>
|
||||
<H3 className="text-4xl md:text-7xl leading-[1.1] tracking-tighter">
|
||||
Inhalte pflegen <br />
|
||||
<span className="text-slate-400 italic font-serif">
|
||||
ohne Angst.
|
||||
</span>
|
||||
</H3>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.2}>
|
||||
<div className="space-y-6">
|
||||
<LeadText className="text-lg md:text-2xl text-slate-500 md:text-slate-400 leading-relaxed max-w-3xl">
|
||||
Vergessen Sie zerschossene Layouts nach einem Textupdate.
|
||||
Meine Websites trennen{" "}
|
||||
<span className="text-slate-900 font-bold underline decoration-blue-500/30 underline-offset-8">
|
||||
Daten von Design
|
||||
</span>
|
||||
.
|
||||
</LeadText>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
|
||||
<div className="lg:col-span-8 relative">
|
||||
<Reveal delay={0.4} direction="up">
|
||||
<CMSVisualizer className="w-full mx-auto" />
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
<Reveal delay={0.5}>
|
||||
<div className="space-y-4">
|
||||
<BodyText className="text-slate-500 leading-relaxed">
|
||||
Durch eine krisenfeste Headless-Architektur (Directus)
|
||||
bewegen Sie sich in einem geschützten Sandkasten – während
|
||||
das Frontend-System die visuelle Integrität Ihrer Marke
|
||||
garantiert.
|
||||
</BodyText>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{["Layout-Schutz", "Live-Vorschau", "Role-RBAC"].map(
|
||||
(tag, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="px-3 py-1 bg-white border border-slate-100 rounded-full text-[10px] font-mono text-slate-400"
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.6}>
|
||||
<div className="p-6 bg-slate-900 rounded-2xl shadow-xl text-[10px] font-mono text-white/50 space-y-3">
|
||||
<div className="flex justify-between items-center text-white/90">
|
||||
<span>PROTOCOL</span>
|
||||
<span className="text-green-500 font-bold">ENFORCED</span>
|
||||
</div>
|
||||
<p className="leading-tight">
|
||||
Website architecture validates all CMS payloads against the
|
||||
design schema before rendering.
|
||||
</p>
|
||||
<div className="pt-2 border-t border-white/10 flex items-center justify-between">
|
||||
<span>INTEGRITY</span>
|
||||
<span className="text-white">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.7}>
|
||||
<div className="p-px w-full bg-gradient-to-r from-transparent via-slate-100 to-transparent" />
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 05: Was Sie bekommen */}
|
||||
<Section
|
||||
number="05"
|
||||
title="Ergebnis"
|
||||
borderTop
|
||||
illustration={<TaskDone className="w-24 h-24" />}
|
||||
>
|
||||
<div className="space-y-12 md:space-y-24">
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<Reveal>
|
||||
<H3 className="text-4xl md:text-7xl leading-[1.1] tracking-tighter">
|
||||
Was Sie konkret <br />
|
||||
<span className="text-slate-400">bekommen.</span>
|
||||
</H3>
|
||||
</Reveal>
|
||||
<Reveal delay={0.2}>
|
||||
<LeadText className="text-lg md:text-2xl text-slate-500 md:text-slate-400 max-w-2xl">
|
||||
Keine halben Sachen. Ich liefere Ihnen ein schlüsselfertiges
|
||||
System mit voller Kontrolle und Transparenz.
|
||||
</LeadText>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal delay={0.3} direction="up">
|
||||
<ResultVisualizer />
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.5}>
|
||||
<div className="pt-10 md:pt-16 border-t border-slate-100 flex flex-col md:flex-row justify-between items-start md:items-center gap-12">
|
||||
<div className="space-y-3">
|
||||
<MonoLabel className="text-blue-500">
|
||||
BEREIT FÜR DEN NÄCHSTEN SCHRITT?
|
||||
</MonoLabel>
|
||||
<div className="text-xl md:text-3xl font-bold tracking-tight text-slate-900">
|
||||
Lassen Sie uns über Ihr Projekt sprechen.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
href="/contact"
|
||||
className="w-full md:w-auto h-16 px-10 text-lg rounded-2xl shadow-2xl shadow-blue-500/10"
|
||||
>
|
||||
Projekt anfragen
|
||||
</Button>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user