chore: overhaul infrastructure and integrate @mintel packages
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s

- Restructure to pnpm monorepo (site moved to apps/web)
- Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config
- Implement Docker service architecture (Varnish, Directus, Gatekeeper)
- Setup environment-aware Gitea Actions deployment
This commit is contained in:
2026-02-05 14:18:51 +01:00
parent 190720ad92
commit 103d71851c
1029 changed files with 13242 additions and 27898 deletions

305
apps/web/app/about/page.tsx Normal file
View File

@@ -0,0 +1,305 @@
import * as React from 'react';
import Image from 'next/image';
import { PageHeader } from '../../src/components/PageHeader';
import { Section } from '../../src/components/Section';
import { Reveal } from '../../src/components/Reveal';
import {
ExperienceIllustration,
ResponsibilityIllustration,
ResultIllustration,
ConceptSystem,
ConceptTarget,
ContactIllustration,
HeroLines,
ParticleNetwork,
GridLines
} from '../../src/components/Landing';
import { Check } from 'lucide-react';
import { H3, H4, LeadText, BodyText, Label, MonoLabel } from '../../src/components/Typography';
import { BackgroundGrid, Card, Container } from '../../src/components/Layout';
import { Button } from '../../src/components/Button';
export default function AboutPage() {
return (
<div className="flex flex-col bg-white overflow-hidden relative">
{/* Background Elements */}
<ParticleNetwork className="opacity-20" />
<BackgroundGrid />
{/* Hero Section */}
<section className="relative pt-32 pb-24 overflow-hidden border-b border-slate-50">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full opacity-10 pointer-events-none">
<HeroLines className="w-full h-full" />
</div>
<div className="absolute right-0 top-0 w-96 h-96 opacity-5 pointer-events-none">
<GridLines />
</div>
<Container variant="narrow" className="relative z-10">
<div className="flex flex-col items-center text-center space-y-12">
<Reveal>
<div className="relative">
{/* Structural rings around avatar */}
<div className="absolute inset-0 -m-8 border border-slate-100 rounded-full animate-[spin_30s_linear_infinite] opacity-50" />
<div className="absolute inset-0 -m-4 border border-slate-200 rounded-full animate-[spin_20s_linear_infinite_reverse] opacity-30" />
<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">
<img
src="/header.webp"
alt="Marc Mintel"
className="w-full h-full object-cover grayscale transition-all duration-1000 ease-in-out scale-110 group-hover:scale-100 group-hover:grayscale-0"
/>
</div>
</div>
</div>
</Reveal>
<div className="space-y-6 max-w-3xl">
<Reveal delay={0.1}>
<div className="flex items-center justify-center gap-4 mb-4">
<div className="h-px w-8 bg-slate-900"></div>
<MonoLabel className="text-slate-900">Digital Architect</MonoLabel>
<div className="h-px w-8 bg-slate-900"></div>
</div>
</Reveal>
<PageHeader
title={<>Über <span className="text-slate-200">mich.</span></>}
description="Warum ich tue, was ich tue und wie Sie davon profitieren."
className="pt-0 md:pt-0"
/>
</div>
</div>
</Container>
{/* Connector to first section */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-px h-16 bg-gradient-to-b from-transparent to-slate-200" />
</section>
{/* Section 01: Experience */}
<Section
number="01"
title="Erfahrung"
borderTop
illustration={<ExperienceIllustration className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
15 Jahre Web-Entwicklung. <br />
<span className="text-slate-200">Vom Designer zum Architekten.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<Reveal delay={0.1}>
<div className="space-y-8">
<LeadText className="text-xl md:text-2xl text-slate-400">
Ich habe Agenturen, Konzerne und Startups von innen gesehen. Dabei habe ich gelernt, what really counts: <span className="text-slate-900">Ergebnisse, nicht Prozesse.</span>
</LeadText>
<ul className="space-y-4">
{[
'Komplexe Systeme vereinfacht',
'Performance-Probleme gelöst',
'Nachhaltige Software-Architekturen gebaut'
].map((item, i) => (
<li key={i} className="flex items-center gap-4 group">
<div className="w-1.5 h-1.5 bg-slate-900 rounded-full group-hover:scale-150 transition-transform" />
<BodyText className="text-lg">{item}</BodyText>
</li>
))}
</ul>
</div>
</Reveal>
<Reveal delay={0.2}>
<Card variant="gray" hover={false} padding="normal" className="group">
<H4 className="text-2xl mb-6">Mein Fokus 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: Responsibility */}
<Section
number="02"
title="Verantwortung"
variant="gray"
borderTop
illustration={<ResponsibilityIllustration className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Ich stehe für meine <br />
<span className="text-slate-200">Arbeit gerade.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
<div className="md:col-span-8 space-y-8">
<Reveal delay={0.1}>
<LeadText className="text-xl md:text-2xl text-slate-400">
In der klassischen Agenturwelt verschwindet Verantwortung oft hinter Hierarchien. Bei mir gibt es nur <span className="text-slate-900">einen Ansprechpartner:</span> Mich.
</LeadText>
</Reveal>
<Reveal delay={0.2}>
<Card variant="white" padding="normal" className="flex flex-row items-start gap-6 group">
<div className="w-12 h-12 bg-slate-900 text-white rounded-xl flex items-center justify-center shrink-0 font-bold text-xl group-hover:rotate-12 transition-transform duration-500">!</div>
<BodyText className="text-slate-900 font-medium text-lg md:text-xl leading-relaxed">
Ich übernehme die volle Verantwortung für die technische Umsetzung und Qualität Ihres Projekts. Ohne Ausreden.
</BodyText>
</Card>
</Reveal>
</div>
</div>
</div>
</Section>
{/* Section 03: Systems */}
<Section
number="03"
title="Philosophie"
borderTop
illustration={<ConceptSystem className="w-24 h-24" />}
>
<div className="space-y-16">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Nachhaltigkeit durch <br />
<span className="text-slate-200">sauberen Code.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-8">
<Reveal delay={0.1}>
<LeadText className="text-xl text-slate-400">
Ich baue keine Wegwerf-Produkte. Meine Systeme sind so konzipiert, dass sie mit Ihrem Unternehmen <span className="text-slate-900">wachsen können.</span>
</LeadText>
</Reveal>
<div className="grid grid-cols-2 gap-4">
{['Skalierbar', 'Wartbar', 'Performant', 'Sicher', 'Unabhängig', 'Zukunftssicher'].map((item, i) => (
<Reveal key={i} delay={0.2 + i * 0.05}>
<div className="flex items-center gap-3 group">
<div className="w-5 h-5 rounded-full bg-slate-50 flex items-center justify-center group-hover:bg-slate-900 transition-colors duration-500">
<Check className="w-2.5 h-2.5 text-slate-400 group-hover:text-white" />
</div>
<Label className="text-slate-900">{item}</Label>
</div>
</Reveal>
))}
</div>
</div>
<Reveal delay={0.3}>
<Card variant="dark" padding="normal" className="relative rounded-2xl overflow-hidden group">
<div className="absolute top-0 right-0 w-48 h-48 bg-white/5 -translate-y-24 translate-x-24 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-1000" />
<H4 className="text-white text-2xl mb-6 relative z-10">Kein Vendor Lock-in.</H4>
<LeadText className="text-slate-400 text-lg relative z-10 leading-relaxed">
Sie behalten die volle Kontrolle über Ihren Code und Ihre Daten. Keine Abhängigkeit von proprietären Systemen.
</LeadText>
</Card>
</Reveal>
</div>
</div>
</Section>
{/* Section 04: Result */}
<Section
number="04"
title="Ergebnis"
borderTop
illustration={<ResultIllustration className="w-24 h-24" />}
>
<div className="space-y-16">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Was Sie von mir <br />
<span className="text-slate-200">erwarten können.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
<div className="space-y-8">
<div className="space-y-6">
<Label>Kein:</Label>
<div className="flex flex-wrap gap-3">
{['Agentur-Zirkus', 'Meeting-Marathon', 'Ticket-Wahnsinn', 'CMS-Frust'].map((item, i) => (
<span key={i} className="px-4 py-2 border border-slate-100 rounded-full bg-slate-50/50">
<BodyText className="text-slate-400 line-through text-base mb-0">{item}</BodyText>
</span>
))}
</div>
</div>
</div>
<div className="space-y-8">
<div className="space-y-6">
<Label className="text-slate-900">Sondern:</Label>
<div className="space-y-8">
{[
{ label: 'Direkte Kommunikation', desc: 'Kurze Wege, schnelle Entscheidungen.' },
{ label: 'Echte Expertise', desc: 'Fundiertes Wissen aus 15 Jahren Praxis.' },
{ label: 'Messbare Qualität', desc: 'Code, der hält, was er verspricht.' }
].map((item, i) => (
<Reveal key={i} delay={0.2 + i * 0.1}>
<div className="flex gap-6 items-start group">
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center shrink-0 mt-1 group-hover:scale-110 transition-transform">
<Check className="w-4 h-4 text-white" />
</div>
<div className="space-y-1">
<H4 className="text-xl">{item.label}</H4>
<BodyText className="text-base text-slate-400">{item.desc}</BodyText>
</div>
</div>
</Reveal>
))}
</div>
</div>
</div>
</div>
</div>
</Section>
{/* Section 05: Today */}
<Section
number="05"
title="Kontakt"
variant="gray"
borderTop
illustration={<ContactIllustration className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Bereit für eine <br />
<span className="text-slate-200">Zusammenarbeit?</span>
</H3>
</Reveal>
<Card variant="white" hover={false} padding="large" className="rounded-3xl shadow-xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-96 h-96 bg-slate-50 -translate-y-1/2 translate-x-1/2 rounded-full blur-[80px] group-hover:bg-slate-100 transition-colors duration-1000" />
<div className="relative z-10 space-y-8">
<LeadText className="text-2xl md:text-4xl leading-tight max-w-2xl text-slate-400">
Lassen Sie uns gemeinsam etwas bauen, das <span className="text-slate-900">wirklich funktioniert.</span>
</LeadText>
<div className="pt-4">
<Button href="/contact">
Projekt anfragen
</Button>
</div>
</div>
</Card>
</div>
</Section>
</div>
);
}

View File

@@ -0,0 +1,268 @@
import { NextRequest, NextResponse } from 'next/server';
import { FileExampleManager } from '../../../src/data/fileExamples';
// Simple ZIP creation without external dependencies
class SimpleZipCreator {
private files: Array<{ filename: string; content: string }> = [];
addFile(filename: string, content: string) {
this.files.push({ filename, content });
}
// Create a basic ZIP file structure
create(): number[] {
const encoder = new TextEncoder();
const chunks: number[][] = [];
let offset = 0;
const centralDirectory: Array<{
name: string;
offset: number;
size: number;
compressedSize: number;
}> = [];
// Process each file
for (const file of this.files) {
const contentBytes = Array.from(encoder.encode(file.content));
const filenameBytes = Array.from(encoder.encode(file.filename));
// Local file header
const localHeader: number[] = [];
// Local file header signature (little endian)
localHeader.push(0x50, 0x4b, 0x03, 0x04);
// Version needed to extract
localHeader.push(20, 0);
// General purpose bit flag
localHeader.push(0, 0);
// Compression method (0 = store)
localHeader.push(0, 0);
// Last modified time/date
localHeader.push(0, 0, 0, 0);
// CRC32 (0 for simplicity)
localHeader.push(0, 0, 0, 0);
// Compressed size
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
// Uncompressed size
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
// Filename length
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
// Extra field length
localHeader.push(0, 0);
// Add filename
localHeader.push(...filenameBytes);
chunks.push(localHeader);
chunks.push(contentBytes);
// Store info for central directory
centralDirectory.push({
name: file.filename,
offset: offset,
size: contentBytes.length,
compressedSize: contentBytes.length
});
offset += localHeader.length + contentBytes.length;
}
// Central directory
const centralDirectoryChunks: number[][] = [];
let centralDirectoryOffset = offset;
for (const entry of centralDirectory) {
const filenameBytes = Array.from(encoder.encode(entry.name));
const centralHeader: number[] = [];
// Central directory header signature
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
// Version made by
centralHeader.push(20, 0);
// Version needed to extract
centralHeader.push(20, 0);
// General purpose bit flag
centralHeader.push(0, 0);
// Compression method
centralHeader.push(0, 0);
// Last modified time/date
centralHeader.push(0, 0, 0, 0);
// CRC32
centralHeader.push(0, 0, 0, 0);
// Compressed size
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
// Uncompressed size
centralHeader.push(...intToLittleEndian(entry.size, 4));
// Filename length
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
// Extra field length
centralHeader.push(0, 0);
// File comment length
centralHeader.push(0, 0);
// Disk number start
centralHeader.push(0, 0);
// Internal file attributes
centralHeader.push(0, 0);
// External file attributes
centralHeader.push(0, 0, 0, 0);
// Relative offset of local header
centralHeader.push(...intToLittleEndian(entry.offset, 4));
// Add filename
centralHeader.push(...filenameBytes);
centralDirectoryChunks.push(centralHeader);
}
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
// End of central directory
const endOfCentralDirectory: number[] = [];
// End of central directory signature
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
// Number of this disk
endOfCentralDirectory.push(0, 0);
// Number of the disk with the start of the central directory
endOfCentralDirectory.push(0, 0);
// Total number of entries on this disk
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
// Total number of entries
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
// Size of the central directory
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
// Offset of start of central directory
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
// ZIP file comment length
endOfCentralDirectory.push(0, 0);
// Combine all chunks
const result: number[] = [];
chunks.forEach(chunk => result.push(...chunk));
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
result.push(...endOfCentralDirectory);
return result;
}
}
// Helper function to convert integer to little endian bytes
function intToLittleEndian(value: number, bytes: number): number[] {
const result: number[] = [];
for (let i = 0; i < bytes; i++) {
result.push((value >> (i * 8)) & 0xff);
}
return result;
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fileIds } = body;
if (!Array.isArray(fileIds) || fileIds.length === 0) {
return NextResponse.json(
{ error: 'fileIds array is required and must not be empty' },
{ status: 400 }
);
}
// Get file contents
const files = await Promise.all(
fileIds.map(async (id) => {
const file = await FileExampleManager.getFileExample(id);
if (!file) {
throw new Error(`File with id ${id} not found`);
}
return file;
})
);
// Create ZIP
const zipCreator = new SimpleZipCreator();
files.forEach(file => {
zipCreator.addFile(file.filename, file.content);
});
const zipData = zipCreator.create();
const buffer = Buffer.from(new Uint8Array(zipData));
// Return ZIP file
return new Response(buffer, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
'Cache-Control': 'no-cache',
}
});
} catch (error) {
console.error('ZIP download error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return NextResponse.json(
{ error: 'Failed to create zip file', details: errorMessage },
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const fileId = searchParams.get('id');
if (!fileId) {
return NextResponse.json(
{ error: 'id parameter is required' },
{ status: 400 }
);
}
const file = await FileExampleManager.getFileExample(fileId);
if (!file) {
return NextResponse.json(
{ error: 'File not found' },
{ status: 404 }
);
}
const encoder = new TextEncoder();
const content = encoder.encode(file.content);
const buffer = Buffer.from(content);
return new Response(buffer, {
headers: {
'Content-Type': getMimeType(file.language),
'Content-Disposition': `attachment; filename="${file.filename}"`,
'Cache-Control': 'no-cache',
}
});
} catch (error) {
console.error('File download error:', error);
return NextResponse.json(
{ error: 'Failed to download file' },
{ status: 500 }
);
}
}
// Helper function to get MIME type
function getMimeType(language: string): string {
const mimeTypes: Record<string, string> = {
'python': 'text/x-python',
'typescript': 'text/x-typescript',
'javascript': 'text/javascript',
'dockerfile': 'text/x-dockerfile',
'yaml': 'text/yaml',
'json': 'application/json',
'html': 'text/html',
'css': 'text/css',
'sql': 'text/x-sql',
'bash': 'text/x-shellscript',
'text': 'text/plain'
};
return mimeTypes[language] || 'text/plain';
}

View File

@@ -0,0 +1,89 @@
import { ImageResponse } from 'next/og';
import { blogPosts } from '../../../../src/data/blogPosts';
export const runtime = 'edge';
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const { slug: slugArray } = await params;
const slug = slugArray?.[0] || 'home';
let title: string;
let description: string;
if (slug === 'home') {
title = 'Marc Mintel';
description = 'Technical problem solver\'s blog - practical insights and learning notes';
} else {
const post = blogPosts.find(p => p.slug === slug);
title = post?.title || 'Marc Mintel';
description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100);
}
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'center',
backgroundColor: '#fff',
padding: '60px',
}}
>
<div
style={{
position: 'absolute',
top: '60px',
right: '60px',
width: '120px',
height: '4px',
backgroundColor: '#3b82f6',
borderRadius: '2px',
}}
/>
<div
style={{
fontSize: '48px',
fontWeight: 700,
color: '#1e293b',
marginBottom: '20px',
fontFamily: 'sans-serif',
}}
>
{title}
</div>
<div
style={{
fontSize: '24px',
fontWeight: 400,
color: '#64748b',
marginBottom: 'auto',
fontFamily: 'sans-serif',
}}
>
{description}
</div>
<div
style={{
fontSize: '18px',
fontWeight: 500,
color: '#94a3b8',
fontFamily: 'sans-serif',
}}
>
mintel.me
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
}

View File

@@ -0,0 +1,199 @@
import * as React from 'react';
import { notFound } from 'next/navigation';
import { blogPosts } from '../../../src/data/blogPosts';
import { Tag } from '../../../src/components/Tag';
import { CodeBlock } from '../../../src/components/ArticleBlockquote';
import { H2 } from '../../../src/components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph';
import { UL, LI } from '../../../src/components/ArticleList';
import { FileExamplesList } from '../../../src/components/FileExamplesList';
import { FileExampleManager } from '../../../src/data/fileExamples';
import { BlogPostClient } from '../../../src/components/BlogPostClient';
import { PageHeader } from '../../../src/components/PageHeader';
import { Section } from '../../../src/components/Section';
import { Reveal } from '../../../src/components/Reveal';
export async function generateStaticParams() {
return blogPosts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = blogPosts.find((p) => p.slug === slug);
if (!post) {
notFound();
}
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
});
const wordCount = post.description.split(/\s+/).length + 100;
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
// Load file examples for the post
let groups: any[] = [];
if (showFileExamples) {
const allGroups = await FileExampleManager.getAllGroups();
groups = allGroups
.map((group) => ({
...group,
files: group.files.filter((file) => {
if (file.postSlug !== slug) return false;
return true;
}),
}))
.filter((group) => group.files.length > 0);
}
return (
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
<BlogPostClient readingTime={readingTime} title={post.title} />
<PageHeader
title={post.title}
description={post.description}
backLink={{ href: '/blog', label: 'Zurück zum Blog' }}
backgroundSymbol="B"
/>
<main id="post-content">
<Section number="01" title="Inhalt">
<div className="prose prose-slate max-w-none">
<div className="flex flex-wrap items-center gap-4 text-[10px] font-bold text-slate-400 mb-12 uppercase tracking-[0.2em]">
<time dateTime={post.date}>{formattedDate}</time>
<span className="text-slate-200"></span>
<span>{readingTime} min read</span>
</div>
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-12">
{post.tags.map((tag, index) => (
<Tag key={tag} tag={tag} index={index} className="text-xs" />
))}
</div>
)}
{slug === 'first-note' && (
<>
<LeadParagraph>
This blog is a public notebook. It's where I document things I learn, problems I solve, and tools I test.
</LeadParagraph>
<H2>Why write in public?</H2>
<Paragraph>
I forget things. Writing them down helps. Making them public helps me think more clearly and might help someone else.
</Paragraph>
<H2>What to expect</H2>
<UL>
<LI>Short entries, usually under 500 words</LI>
<LI>Practical solutions to specific problems</LI>
<LI>Notes on tools and workflows</LI>
<LI>Mistakes and what I learned</LI>
</UL>
</>
)}
{slug === 'debugging-tips' && (
<>
<LeadParagraph>
Sometimes the simplest debugging tool is the best one. Print statements get a bad reputation, but they're often exactly what you need.
</LeadParagraph>
<H2>Why print statements work</H2>
<Paragraph>
Debuggers are powerful, but they change how your code runs. Print statements don't.
</Paragraph>
<CodeBlock language="python" showLineNumbers={true}>
{`def process_data(data):
print(f"Processing {len(data)} items")
result = expensive_operation(data)
print(f"Operation result: {result}")
return result`}
</CodeBlock>
<H2>Complete examples</H2>
<Paragraph>
Here are some practical file examples you can copy and download. These include proper error handling and logging.
</Paragraph>
<div className="my-8">
<FileExamplesList groups={groups} />
</div>
</>
)}
{slug === 'architecture-patterns' && (
<>
<LeadParagraph>
Good software architecture is about making the right decisions early. Here are some patterns I've found useful in production systems.
</LeadParagraph>
<H2>Repository Pattern</H2>
<Paragraph>
The repository pattern provides a clean separation between your business logic and data access layer. It makes your code more testable and maintainable.
</Paragraph>
<H2>Service Layer</H2>
<Paragraph>
Services orchestrate business logic and coordinate between repositories and domain events. They keep your controllers thin and your business rules organized.
</Paragraph>
<H2>Domain Events</H2>
<Paragraph>
Domain events help you decouple components and react to changes in your system. They're essential for building scalable, event-driven architectures.
</Paragraph>
<H2>Complete examples</H2>
<Paragraph>
These TypeScript examples demonstrate modern architecture patterns for scalable applications. You can copy them directly into your project.
</Paragraph>
<div className="my-8">
<FileExamplesList groups={groups} />
</div>
</>
)}
{slug === 'docker-deployment' && (
<>
<LeadParagraph>
Docker has become the standard for containerizing applications. Here's how to set up production-ready deployments that are secure, efficient, and maintainable.
</LeadParagraph>
<H2>Multi-stage builds</H2>
<Paragraph>
Multi-stage builds keep your production images small and secure by separating build and runtime environments. This reduces attack surface and speeds up deployments.
</Paragraph>
<H2>Health checks and monitoring</H2>
<Paragraph>
Proper health checks ensure your containers are running correctly. Combined with restart policies, this gives you resilient, self-healing deployments.
</Paragraph>
<H2>Orchestration with Docker Compose</H2>
<Paragraph>
Docker Compose makes it easy to manage multi-service applications in development and production. Define services, networks, and volumes in a single file.
</Paragraph>
<H2>Complete examples</H2>
<Paragraph>
These Docker configurations are production-ready. Use them as a starting point for your own deployments.
</Paragraph>
<div className="my-8">
<FileExamplesList groups={groups} />
</div>
</>
)}
</div>
</Section>
</main>
</div>
);
}

View File

@@ -0,0 +1,242 @@
import React from 'react';
import { Tag } from '../../../src/components/Tag';
import { H2 } from '../../../src/components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../../src/components/ArticleParagraph';
import { UL, LI } from '../../../src/components/ArticleList';
import { CodeBlock } from '../../../src/components/ArticleBlockquote';
import { YouTubeEmbed } from '../../../src/components/YouTubeEmbed';
import { TwitterEmbed } from '../../../src/components/TwitterEmbed';
import { GenericEmbed } from '../../../src/components/GenericEmbed';
import { Mermaid } from '../../../src/components/Mermaid';
import { BlogPostClient } from '../../../src/components/BlogPostClient';
export default function EmbedDemoPage() {
const post = {
title: "Rich Content Embedding Demo",
description: "Testing our new free embed components for YouTube, Twitter, Mermaid diagrams, and other platforms",
date: "2024-02-15",
slug: "embed-demo",
tags: ["embeds", "components", "tutorial", "mermaid"]
};
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
});
const readingTime = 5;
return (
<div className="min-h-screen bg-white">
<BlogPostClient readingTime={readingTime} title={post.title} />
<main id="post-content" className="pt-24">
<section className="py-12 md:py-16">
<div className="max-w-3xl mx-auto px-6">
<div className="text-center">
<h1 className="text-3xl md:text-5xl font-serif font-bold text-slate-900 mb-6 leading-tight tracking-tight">
{post.title}
</h1>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 mb-6 font-sans">
<time dateTime={post.date} className="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zM4 8h12v8H4V8z"/>
</svg>
{formattedDate}
</time>
<span className="text-slate-400"></span>
<span className="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full">
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd"/>
</svg>
{readingTime} min
</span>
</div>
<p className="text-xl md:text-2xl text-slate-600 leading-relaxed font-serif italic mb-8 max-w-2xl mx-auto">
{post.description}
</p>
<div className="flex flex-wrap justify-center gap-2 mb-8">
{post.tags.map((tag, index) => (
<Tag key={tag} tag={tag} index={index} className="text-xs" />
))}
</div>
</div>
</div>
</section>
<section className="max-w-3xl mx-auto px-6 pb-24">
<div className="prose prose-slate max-w-none">
<LeadParagraph>
This post demonstrates our new free embed components that give you full styling control over YouTube videos, Twitter tweets, and other rich content - all generated at build time.
</LeadParagraph>
<H2>YouTube Embed Example</H2>
<Paragraph>
Here's a YouTube video embedded with full styling control. The component uses build-time generation for optimal performance.
</Paragraph>
<div className="my-6">
<YouTubeEmbed
videoId="dQw4w9WgXcQ"
title="Demo Video"
style="minimal"
className="my-4"
/>
</div>
<Paragraph>
You can customize the appearance using CSS variables or data attributes:
</Paragraph>
<CodeBlock
language="jsx"
showLineNumbers={true}
>
{`<YouTubeEmbed
videoId="dQw4w9WgXcQ"
style="minimal" // 'default' | 'minimal' | 'rounded' | 'flat'
aspectRatio="56.25%" // Custom aspect ratio
className="my-4" // Additional classes
/>`}
</CodeBlock>
<H2>Twitter/X Embed Example</H2>
<Paragraph>
Twitter embeds use the official Twitter iframe embed for reliable display.
</Paragraph>
<div className="my-4">
<TwitterEmbed
tweetId="20"
theme="light"
align="center"
/>
</div>
<CodeBlock
language="jsx"
showLineNumbers={true}
>
{`<TwitterEmbed
tweetId="20"
theme="light" // 'light' | 'dark'
align="center" // 'left' | 'center' | 'right'
/>`}
</CodeBlock>
<H2>Generic Embed Example</H2>
<Paragraph>
The generic component supports direct embeds for Vimeo, CodePen, GitHub Gists, and other platforms.
</Paragraph>
<div className="my-6">
<GenericEmbed
url="https://vimeo.com/123456789"
type="video"
maxWidth="800px"
/>
</div>
<CodeBlock
language="jsx"
showLineNumbers={true}
>
{`<GenericEmbed
url="https://vimeo.com/123456789"
type="video" // 'video' | 'article' | 'rich'
maxWidth="800px"
/>`}
</CodeBlock>
<H2>Mermaid Diagrams</H2>
<Paragraph>
We've added support for Mermaid diagrams! You can now create flowcharts, sequence diagrams, and more using a simple text-based syntax.
</Paragraph>
<div className="my-8">
<Mermaid
graph={`graph LR
A[Client] --> B[Load Balancer]
B --> C[App Server 1]
B --> D[App Server 2]
C --> E[(Database)]
D --> E`}
/>
</div>
<Paragraph>
Usage is straightforward:
</Paragraph>
<CodeBlock
language="jsx"
showLineNumbers={true}
>
{`<Mermaid
graph={\`graph LR
A[Client] --> B[Load Balancer]
B --> C[App Server 1]
B --> D[App Server 2]
C --> E[(Database)]
D --> E\`}
/>`}
</CodeBlock>
<H2>Styling Control</H2>
<Paragraph>
All components use CSS variables for easy customization:
</Paragraph>
<CodeBlock
language="css"
showLineNumbers={true}
>
{`.youtube-embed {
--aspect-ratio: 56.25%;
--bg-color: #000000;
--border-radius: 12px;
--shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Data attribute variations */
.youtube-embed[data-style="minimal"] {
--border-radius: 4px;
--shadow: none;
}`}
</CodeBlock>
<H2>Benefits</H2>
<UL>
<LI><strong>Free:</strong> No paid services required</LI>
<LI><strong>Fast:</strong> Build-time generation, no runtime API calls</LI>
<LI><strong>Flexible:</strong> Full styling control via CSS variables</LI>
<LI><strong>Self-hosted:</strong> Complete ownership and privacy</LI>
<LI><strong>SEO-friendly:</strong> Static HTML content</LI>
</UL>
<H2>Integration</H2>
<Paragraph>
Simply import the components in your blog posts:
</Paragraph>
<CodeBlock
language="jsx"
showLineNumbers={true}
>
{`import { YouTubeEmbed } from '../components/YouTubeEmbed';
import { TwitterEmbed } from '../components/TwitterEmbed';
import { GenericEmbed } from '../components/GenericEmbed';
<YouTubeEmbed videoId="abc123" style="rounded" />
<TwitterEmbed tweetId="123456789" theme="dark" />`}
</CodeBlock>
</div>
</section>
</main>
</div>
);
}

106
apps/web/app/blog/page.tsx Normal file
View File

@@ -0,0 +1,106 @@
'use client';
import * as React from 'react';
import { useState, useEffect } from 'react';
import { MediumCard } from '../../src/components/MediumCard';
import { SearchBar } from '../../src/components/SearchBar';
import { Tag } from '../../src/components/Tag';
import { blogPosts } from '../../src/data/blogPosts';
import { PageHeader } from '../../src/components/PageHeader';
import { Reveal } from '../../src/components/Reveal';
export default function BlogPage() {
const [searchQuery, setSearchQuery] = useState('');
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
// Sort posts by date
const allPosts = [...blogPosts].sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
// Get unique tags
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
useEffect(() => {
const query = searchQuery.toLowerCase().trim();
if (query.startsWith('#')) {
const tag = query.slice(1);
setFilteredPosts(allPosts.filter(post =>
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
));
} else {
setFilteredPosts(allPosts.filter(post => {
const title = post.title.toLowerCase();
const description = post.description.toLowerCase();
const tags = (post.tags || []).join(' ').toLowerCase();
return title.includes(query) || description.includes(query) || tags.includes(query);
}));
}
}, [searchQuery]);
const filterByTag = (tag: string) => {
setSearchQuery(`#${tag}`);
};
return (
<div className="flex flex-col gap-24 py-12 md:py-24 overflow-hidden">
<PageHeader
title={<>Blog <br /><span className="text-slate-200">& Notes.</span></>}
description="A public notebook of things I figured out, mistakes I made, and tools I tested."
backgroundSymbol="B"
/>
<section className="narrow-container">
<div className="grid grid-cols-1 md:grid-cols-12 gap-16">
{/* Sidebar / Filter area */}
<div className="md:col-span-4">
<div className="sticky top-32 space-y-16">
<Reveal>
<div className="space-y-4">
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Suchen</h3>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
</div>
</Reveal>
{allTags.length > 0 && (
<Reveal delay={0.2}>
<div className="space-y-6">
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-slate-400">Themen</h3>
<div className="flex flex-wrap gap-2">
{allTags.map((tag, index) => (
<button
key={tag}
onClick={() => filterByTag(tag)}
className="text-left"
>
<Tag tag={tag} index={index} />
</button>
))}
</div>
</div>
</Reveal>
)}
</div>
</div>
{/* Posts area */}
<div className="md:col-span-8">
<div id="posts-container" className="flex flex-col gap-8">
{filteredPosts.length === 0 ? (
<div className="empty-state">
<p>No posts found matching your criteria.</p>
</div>
) : (
filteredPosts.map((post, i) => (
<Reveal key={post.slug} delay={0.1 * i} width="100%">
<MediumCard post={post} />
</Reveal>
))
)}
</div>
</div>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,491 @@
'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 { MotionButton } from '../../../src/components/Button';
import { IframeSection } from '../../../src/components/IframeSection';
import {
Activity,
Database,
Layout,
Users,
ArrowRight,
Zap,
ShieldCheck,
Globe2,
Settings,
Search,
Monitor,
Cpu,
Server,
Layers
} from 'lucide-react';
/**
* TECHNICAL MARKER COMPONENT
* Implements the "hand-drawn marker" effect from STYLEGUIDE.md
* Updated: Only yellow marker as requested.
*/
const Marker: React.FC<{ children: React.ReactNode; delay?: number }> = ({
children,
delay = 0
}) => {
return (
<span className="relative inline-block px-1">
<motion.span
initial={{ scaleX: 0, opacity: 0 }}
whileInView={{ scaleX: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.2, delay: delay + 0.1, ease: [0.23, 1, 0.32, 1] }}
className="absolute inset-0 z-[-1] -skew-x-6 rotate-[-1deg] translate-y-1 transform-gpu bg-[rgba(255,235,59,0.95)] origin-left"
aria-hidden="true"
/>
{children}
</span>
);
};
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-[40vh] 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">
<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-12">
<Reveal delay={0.1} direction="up" scale={0.98} blur>
<H1 className="text-6xl md:text-8xl tracking-tighter leading-[0.9] font-bold text-slate-900">
KLZ Cables<br />
<span className="text-slate-100">Case Study.</span>
</H1>
</Reveal>
<Reveal delay={0.2} direction="right" blur>
<div className="max-w-3xl border-l-[3px] border-slate-900 pl-8 md:pl-12">
<LeadText className="text-2xl md:text-4xl leading-tight text-slate-900 font-medium">
Engineering eines <br />
<Marker delay={0.2}>B2B Commerce Systems.</Marker>
</LeadText>
<BodyText className="mt-6 text-lg 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-12 md:gap-24 pt-12 border-t border-slate-100">
<div className="space-y-2">
<Label className="text-slate-400">Data Integrity</Label>
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 bg-[rgba(129,199,132,1)] rounded-full animate-pulse" />
<span className="text-2xl font-bold font-mono text-slate-900 tracking-tight">Relational Data</span>
</div>
</div>
<div className="space-y-2">
<Label className="text-slate-400">Security Layer</Label>
<div className="flex items-center gap-3 text-2xl font-bold font-mono text-slate-900">
<ShieldCheck className="w-6 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"
>
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 lg:gap-24 items-start">
<div className="md:col-span-12 mb-12">
<Reveal direction="left" blur>
<H2 className="text-5xl md:text-8xl tracking-tighter mb-12">
Architektur- <br />Refactor.
</H2>
</Reveal>
</div>
<div className="md:col-span-7 space-y-12">
<Reveal delay={0.1} direction="up" blur>
<div className="space-y-10">
<BodyText className="text-2xl leading-relaxed font-serif italic text-slate-500">
Vom statischen HTML zur zentralen Daten-Instanz.
</BodyText>
<BodyText className="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 als <Marker delay={0.3}>Headless JSON-Provider</Marker>. Durch die Implementierung nativer PHP-Microservices und den Verzicht auf volatile Drittanbieter-Plugins wurde ein System geschaffen, das keine technologischen Überraschungen zulässt. <Marker delay={0.5}>Stability by Design.</Marker>
</BodyText>
</div>
</Reveal>
</div>
<div className="md:col-span-5 relative">
<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-12 bg-slate-50 rounded-[3rem] border border-slate-100 space-y-12 relative overflow-hidden group shadow-sm text-left"
>
<div className="space-y-8 relative z-10">
<Label className="text-slate-900">System Metriken</Label>
<div className="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) => (
<motion.div
key={i}
initial={{ x: -20, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
transition={{ delay: 0.5 + (i * 0.1), duration: 0.5 }}
className="flex 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-[10px] text-slate-400">{item.label}</MonoLabel>
<BodyText className="text-base font-bold text-slate-900">{item.desc}</BodyText>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
</Reveal>
</div>
</div>
</Section>
{/* --- SHOWCASE: LANDING --- */}
<section className="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-16 flex justify-between items-end">
<div className="space-y-6">
<Label className="text-slate-500">Infrastructure Validation</Label>
<H3 className="text-5xl md:text-8xl tracking-tighter">Global Hub.</H3>
</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="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-16">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-end">
<Reveal direction="left" blur>
<div className="space-y-6">
<Label className="text-slate-400">Asset Pipelines</Label>
<H3 className="text-4xl md:text-6xl tracking-tighter">Automated Documentation.</H3>
</div>
</Reveal>
<Reveal delay={0.1} direction="right" blur>
<BodyText className="text-xl text-slate-500 pb-2 font-serif italic">
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="1000px"
desktopWidth={1920}
allowScroll
offsetY={100}
browserFrame
className="w-full transition-all duration-1000 no-scrollbar"
/>
</div>
</motion.div>
</Reveal>
</div>
</Section>
{/* --- SECTION 03: COMMERCE --- */}
<Section
number="03"
title="Katalog-Architektur"
borderBottom
containerVariant="wide"
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
<div className="lg:col-span-12 mb-12 text-center lg:text-left relative z-10">
<Reveal direction="down" blur>
<H3 className="text-4xl md:text-6xl max-w-4xl tracking-tighter">
Fokus auf <br /><Marker delay={0.2}>Spezifikationen.</Marker>
</H3>
</Reveal>
</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-[650px] w-full overflow-visible 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-6">
<Label className="text-slate-400">Katalog-Struktur</Label>
<LeadText className="text-lg">
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-8 bg-white border border-slate-200 rounded-3xl shadow-sm"
>
<Layers className="w-6 h-6 text-slate-400 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-4xl 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-[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-16 text-center">
<Reveal direction="up" blur>
<H3 className="text-5xl md:text-8xl tracking-tighter">System-Lifecycle.</H3>
<LeadText className="mx-auto max-w-2xl pt-6 text-xl">
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="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"
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
<div className="lg:col-span-7">
<Reveal direction="left" scale={0.98} blur>
<motion.div
whileHover={{ scale: 1.01 }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
className="relative h-[800px] w-full"
>
<IframeSection
src="/showcase/klz-cables.com/contact.html"
height="100%"
desktopWidth={1200}
allowScroll
browserFrame
className="h-full w-full no-scrollbar"
/>
</motion.div>
</Reveal>
</div>
<div className="lg:col-span-5 space-y-10">
<Reveal direction="right" blur>
<div className="space-y-6">
<Label className="text-slate-400">Conversion Layer</Label>
<H3 className="text-4xl md:text-6xl tracking-tighter">Direkter Draht.</H3>
<BodyText className="text-xl text-slate-500 font-serif italic">
Das Kontakt-System wurde auf maximale Reduktion getrimmt. Keine unnötigen Hürden, sondern ein direkter Kommunikations-Kanal zwischen technischem Bedarf und individueller Beratung.
</BodyText>
</div>
</Reveal>
</div>
</div>
</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-12">
<Reveal direction="left" blur>
<div className="space-y-4">
<MonoLabel className="text-slate-400 tracking-[0.4em]">CONSULTING // ENGINEERING</MonoLabel>
<H2 className="text-6xl 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-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-10 md:p-14 rounded-[3.5rem] space-y-12 relative group shadow-sm">
<Reveal direction="right" blur>
<div className="inline-flex items-center gap-3 px-4 py-2 bg-white rounded-full border border-slate-200 mb-4 font-mono 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-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-3 group/item">
<MonoLabel className="text-[10px] text-slate-400 group-hover/item:text-slate-900 transition-colors duration-500">{item.title}</MonoLabel>
<BodyText className="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">
<MotionButton href="/contact" variant="outline" className="w-full py-8 text-lg group border-2 border-slate-900 rounded-full bg-white hover:bg-slate-900 hover:text-white transition-all duration-700">
System-Analyse anfragen
<ArrowRight className="inline-block ml-4 w-6 h-6 group-hover:translate-x-4 transition-transform duration-700" />
</MotionButton>
</Reveal>
</div>
</div>
</Container>
</section>
</div>
);
}

View File

@@ -0,0 +1,88 @@
'use client';
import React from 'react';
import { PageHeader } from '../../src/components/PageHeader';
import { Section } from '../../src/components/Section';
import { Reveal } from '../../src/components/Reveal';
import { H3, LeadText, Label } from '../../src/components/Typography';
import { BackgroundGrid, Card, Container } from '../../src/components/Layout';
import { MotionButton } from '../../src/components/Button';
import Image from 'next/image';
export default function CaseStudiesPage() {
return (
<div className="flex flex-col bg-white overflow-hidden relative min-h-screen">
<BackgroundGrid />
<PageHeader
title={<>Case Studies: <br /><span className="text-slate-200">Qualität in jedem Detail.</span></>}
description="Ein Blick hinter die Kulissen ausgewählter Projekte. Von der ersten Idee bis zum fertigen Hochleistungssystem."
backLink={{ href: '/', label: 'Zurück' }}
backgroundSymbol="C"
/>
<Section number="01" title="Projekte" borderTop>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<Reveal>
<Card variant="white" className="group overflow-hidden">
<div className="aspect-video relative overflow-hidden rounded-xl mb-8 bg-slate-100 border border-slate-100">
{/* We'll use a placeholder or a screenshot if available.
Since we have the cloned site, we could technically iframe a preview here too,
but a static image or a styled div is more standard for a card. */}
<div className="absolute inset-0 flex items-center justify-center bg-[#0117bf] transition-transform duration-700 group-hover:scale-105 p-12">
<Image
src="/showcase/klz-cables/assets/img/white_logo_transparent_background.svg"
alt="KLZ Cables Logo"
width={200}
height={200}
className="w-full h-auto max-w-[240px]"
/>
</div>
</div>
<div className="space-y-4">
<Label>Infrastructure & Energy</Label>
<H3 className="group-hover:text-slate-900 transition-colors">KLZ Cables Digitaler Netzbau</H3>
<LeadText className="text-base line-clamp-3">
Wie wir eine komplexe WordPress-Struktur in ein performantes, sauberes und langlebiges Web-System verwandelt haben. Fokus auf Performance, SEO und Benutzerführung.
</LeadText>
<div className="pt-4">
<MotionButton href="/case-studies/klz-cables">
Case Study lesen
</MotionButton>
</div>
</div>
</Card>
</Reveal>
<Reveal delay={0.2}>
<div className="h-full flex flex-col justify-center border-2 border-dashed border-slate-100 rounded-3xl p-12 text-center space-y-4">
<Label>Demnächst</Label>
<H3 className="text-slate-200">Weitere Projekte sind in Arbeit.</H3>
<LeadText className="text-base italic">
Ich dokumentiere gerade weitere spannende Projekte aus den Bereichen SaaS, E-Commerce und Systemarchitektur.
</LeadText>
</div>
</Reveal>
</div>
</Section>
<Section number="02" title="Philosophie" borderTop variant="gray">
<div className="max-w-3xl space-y-8">
<Reveal>
<H3 className="text-4xl leading-tight">
Warum ich Case Studies zeige? <br />
<span className="text-slate-200">Weil Code mehr als Text ist.</span>
</H3>
</Reveal>
<Reveal delay={0.2}>
<LeadText className="text-xl">
In diesen Case Studies geht es nicht nur um bunte Bilder. Es geht um die technischen Entscheidungen, die ein Projekt erfolgreich machen. Schnelle Ladezeiten, SEO-Exzellenz und wartbarer Code sind keine Zufälle, sondern das Ergebnis von präziser Planung.
</LeadText>
</Reveal>
</div>
</Section>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import * as React from 'react';
import { Reveal } from '../../src/components/Reveal';
import { PageHeader } from '../../src/components/PageHeader';
import { Section } from '../../src/components/Section';
import { ContactForm } from '../../src/components/ContactForm';
export default function ContactPage() {
return (
<div className="flex flex-col gap-12 py-12 md:py-24">
<PageHeader
title={<>Projekt <br /><span className="text-slate-200">konfigurieren.</span></>}
description="Nutzen Sie den Konfigurator für eine erste Einschätzung oder schreiben Sie mir direkt eine Email."
backLink={{ href: '/', label: 'Zurück' }}
backgroundSymbol="?"
/>
<Section number="01" title="Konfigurator" containerVariant="wide" className="!py-12">
<ContactForm />
</Section>
<Section number="02" title="Direkt" className="!py-12">
<div className="grid grid-cols-1 gap-24">
<Reveal delay={0.4}>
<div className="space-y-8">
<a
href="mailto:marc@mintel.me"
className="group block space-y-2"
>
<span className="text-xs font-bold uppercase tracking-widest text-slate-300 group-hover:text-slate-900 transition-colors">Email</span>
<p className="text-3xl md:text-6xl font-bold text-slate-900 border-b border-slate-100 group-hover:border-slate-900 transition-all duration-500 pb-6 tracking-tighter">
marc@mintel.me
</p>
</a>
</div>
</Reveal>
</div>
</Section>
</div>
);
}

692
apps/web/app/globals.css Normal file
View File

@@ -0,0 +1,692 @@
@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-6xl md:text-8xl leading-[0.95] mb-12;
}
h2 {
@apply text-4xl md:text-6xl leading-tight mb-8 mt-16;
}
h3 {
@apply text-3xl md:text-5xl leading-tight mb-6 mt-12;
}
h4 {
@apply text-2xl md:text-3xl leading-tight mb-4 mt-8;
}
p {
@apply mb-4 text-base leading-relaxed text-slate-700;
}
.lead {
@apply text-xl 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-xl 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-6 py-12;
}
.wide-container {
@apply max-w-7xl mx-auto px-6 py-16;
}
.narrow-container {
@apply max-w-4xl mx-auto px-6 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 text-lg leading-relaxed;
}
.article-content p {
@apply mb-5;
}
.article-content h2 {
@apply text-2xl font-bold mt-8 mb-3;
}
.article-content h3 {
@apply text-xl font-bold mt-6 mb-2;
}
.article-content ul,
.article-content ol {
@apply ml-6 mb-5;
}
.article-content li {
@apply mb-1;
}
.article-content blockquote {
@apply border-l-2 border-slate-400 pl-4 italic text-slate-600 my-5 text-lg;
}
/* Buttons */
.btn {
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm;
}
.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.70) 20%,
rgba(253, 230, 138, 0.70) 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.00) 0%,
rgba(253, 230, 138, 0.60) 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.10);
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;
}

51
apps/web/app/layout.tsx Normal file
View File

@@ -0,0 +1,51 @@
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}`}>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body className="min-h-screen bg-white">
<Header />
<main>
{children}
</main>
<Footer />
<InteractiveElements />
<Analytics />
</body>
</html>
);
}

315
apps/web/app/page.tsx Normal file
View File

@@ -0,0 +1,315 @@
import {
ComparisonRow,
ConceptAutomation,
ConceptCode,
ConceptCommunication,
ConceptMessy,
ConceptPrice,
ConceptPrototyping,
ConceptSystem,
ConceptWebsite,
DifferenceIllustration,
HeroArchitecture,
HeroMainIllustration
} 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 { BackgroundGrid, Card, Container } from '../src/components/Layout';
import { Button } from '../src/components/Button';
export default function LandingPage() {
return (
<div className="flex flex-col bg-white overflow-hidden relative">
<BackgroundGrid />
{/* Hero Section */}
<section className="relative min-h-[80vh] flex items-center pt-24 md:pt-0">
<Container variant="narrow" className="relative">
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-24 items-center">
{/* Left Column */}
<div className="md:col-span-6 relative z-10">
<Reveal>
<div className="space-y-8">
<div className="flex items-center gap-4">
<div className="w-8 h-px bg-slate-900"></div>
<MonoLabel className="text-slate-900">Digital Architect</MonoLabel>
</div>
<H1 className="text-6xl md:text-8xl">
Websites <br />
<span className="text-slate-200">ohne Overhead.</span>
</H1>
<div className="pt-4">
<Button href="#contact" variant="outline">
Projekt anfragen
</Button>
</div>
</div>
</Reveal>
</div>
{/* Right Column */}
<div className="md:col-span-6 relative h-[400px] md:h-[600px] flex items-center justify-center">
<div className="absolute inset-0 -z-10 opacity-[0.03] pointer-events-none flex items-center justify-center scale-150">
<HeroArchitecture className="w-full h-full" />
</div>
<Reveal delay={0.2} className="w-full h-full flex items-center justify-center">
<div className="relative w-full h-full flex items-center justify-center pointer-events-none">
<HeroMainIllustration className="w-full h-full scale-110 md:scale-125 origin-center" />
</div>
</Reveal>
</div>
</div>
</Container>
</section>
{/* Section 02: The Promise */}
<Section
number="02"
title="Das Versprechen"
borderTop
>
<div className="space-y-16 relative">
<Reveal>
<H3 className="max-w-3xl">
Schluss mit aufgeblähten Prozessen. <br />
<span className="text-slate-200">Ich reduziere auf das Wesentliche.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10">
<Reveal delay={0.1}>
<div className="space-y-8">
<div className="flex items-center gap-4">
<Label className="text-slate-900">Was ich biete</Label>
</div>
<ul className="space-y-6">
{[
{ text: 'Direkte Kommunikation ohne Umwege', icon: <ConceptCommunication className="w-12 h-12" /> },
{ text: 'Schnelle Prototypen statt langer Konzepte', icon: <ConceptPrototyping className="w-12 h-12" /> },
{ text: 'Sauberer Code, der auch morgen noch läuft', icon: <ConceptCode className="w-12 h-12" /> },
{ text: 'Fixpreise für volle Budgetsicherheit', icon: <ConceptPrice className="w-12 h-12" /> }
].map((item, i) => (
<li key={i} className="flex items-center gap-6 group">
<div className="shrink-0 transition-transform duration-500 group-hover:scale-110">
{item.icon}
</div>
<LeadText className="text-xl">{item.text}</LeadText>
</li>
))}
</ul>
</div>
</Reveal>
<Reveal delay={0.2}>
<div className="space-y-8 opacity-40 hover:opacity-100 transition-opacity duration-700">
<div className="flex items-center gap-4">
<Label>Was ich nicht mache</Label>
</div>
<ul className="space-y-4">
{[
'Endlose Workshops ohne Ergebnis',
'PowerPoint-Schlachten',
'Outsourcing an Billig-Anbieter',
'Wartungsverträge mit versteckten Kosten'
].map((item, i) => (
<li key={i} className="flex items-start gap-3 decoration-slate-200 line-through">
<span className="w-1.5 h-1.5 bg-slate-200 rounded-full mt-2.5 shrink-0"></span>
<LeadText className="text-slate-400 text-lg">{item}</LeadText>
</li>
))}
</ul>
</div>
</Reveal>
</div>
</div>
</Section>
{/* Section 03: The Difference */}
<Section
number="03"
title="Der Unterschied"
variant="white"
borderTop
>
<div className="space-y-16 relative">
<div className="flex flex-col md:flex-row gap-12 items-center">
<Reveal className="flex-1">
<LeadText className="text-2xl md:text-3xl leading-tight max-w-2xl relative z-10 text-slate-400">
Ich arbeite nicht gegen die Zeit, sondern <span className="text-slate-900">für das Ergebnis.</span> Mein Fokus liegt auf der Umsetzung, nicht auf der Verwaltung von Prozessen.
</LeadText>
</Reveal>
<Reveal delay={0.2} className="w-full md:w-72 shrink-0">
<div className="p-6 bg-slate-50 rounded-2xl border border-slate-100">
<DifferenceIllustration className="w-full h-auto grayscale opacity-50" />
</div>
</Reveal>
</div>
<div className="grid grid-cols-1 gap-8 relative z-20">
<ComparisonRow
negativeLabel="Klassisch"
negativeText="Lange Planungsphasen und abstrakte Konzepte."
positiveLabel="Mein Weg"
positiveText="Schnelle Prototypen. Sie sehen Fortschritt in Tagen."
delay={0.1}
/>
<ComparisonRow
negativeLabel="Klassisch"
negativeText="Komplexe Preisstrukturen und versteckte Kosten."
positiveLabel="Mein Weg"
positiveText="Klare Fixpreise. Volle Kostentransparenz."
reverse
delay={0.2}
/>
</div>
</div>
</Section>
{/* Section 04: Target Group */}
<Section
number="04"
title="Zielgruppe"
borderTop
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
<Reveal>
<Card variant="dark" padding="normal" className="group">
<div className="space-y-6 relative overflow-hidden">
<div className="w-16 h-16 bg-white/5 rounded-xl flex items-center justify-center border border-white/10">
<ConceptPrice className="w-8 h-8" />
</div>
<H3 className="text-white text-3xl">Unternehmer & <br/>Geschäftsführer</H3>
<LeadText className="text-slate-400 text-lg">
"Ich brauche eine Lösung, die funktioniert. Ich habe keine Zeit für technische Details."
</LeadText>
</div>
<div className="pt-8 border-t border-white/5 mt-8">
<Label className="group-hover:text-white transition-colors">Perfekt für Sie</Label>
</div>
</Card>
</Reveal>
<Reveal delay={0.2}>
<Card variant="white" padding="normal" className="group">
<div className="space-y-6 relative overflow-hidden">
<div className="w-16 h-16 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-center">
<ConceptWebsite className="w-8 h-8" />
</div>
<H3 className="text-3xl">Marketing & <br/>Vertrieb</H3>
<LeadText className="text-slate-400 text-lg">
"Wir brauchen Landingpages und Tools, um unsere Ziele zu erreichen. Schnell und zuverlässig."
</LeadText>
</div>
<div className="pt-8 border-t border-slate-50 mt-8">
<Label className="group-hover:text-slate-900 transition-colors">Perfekt für Sie</Label>
</div>
</Card>
</Reveal>
</div>
</Section>
{/* Section 05: Services */}
<Section
number="05"
title="Leistungen"
variant="gray"
borderTop
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative z-20">
<Reveal delay={0.1}>
<Card variant="white" padding="small" className="group">
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
<ConceptWebsite className="w-8 h-8" />
</div>
<div className="space-y-4">
<H3 className="text-2xl">Websites</H3>
<BodyText>
High-Performance Websites. Maßgeschneiderte Architektur statt Baukasten.
</BodyText>
<div className="pt-4">
<a href="/websites" className="text-[10px] font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-100 pb-1 hover:border-slate-900 transition-all">
Details
</a>
</div>
</div>
</Card>
</Reveal>
<Reveal delay={0.3}>
<Card variant="white" padding="small" className="group mt-8 md:mt-0">
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
<ConceptSystem className="w-8 h-8" />
</div>
<div className="space-y-4">
<H3 className="text-2xl">Systeme</H3>
<BodyText>
Web-Applikationen, Portale, interne Tools. Wenn Standard an Grenzen stößt.
</BodyText>
</div>
</Card>
</Reveal>
<Reveal delay={0.5}>
<Card variant="white" padding="small" className="group">
<div className="w-16 h-16 bg-slate-50 rounded-xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform duration-500">
<ConceptAutomation className="w-8 h-8" />
</div>
<div className="space-y-4">
<H3 className="text-2xl">Automatisierung</H3>
<BodyText>
Verbindung von Tools, automatische Prozesse, Daten-Synchronisation.
</BodyText>
</div>
</Card>
</Reveal>
</div>
</Section>
{/* Section 06: Contact */}
<Section
number="06"
title="Kontakt"
borderTop
>
<div className="relative py-12" id="contact">
<Reveal>
<div className="space-y-16">
<H1 className="text-6xl md:text-8xl">
Lassen Sie uns <br />
<span className="text-slate-200">starten.</span>
</H1>
<div className="flex flex-col md:flex-row gap-16 items-start relative z-10">
<div className="space-y-8 flex-1">
<LeadText className="text-2xl md:text-3xl text-slate-400">
Schreiben Sie mir kurz, worum es geht. Ich melde mich innerhalb von <span className="text-slate-900">24 Stunden</span>.
</LeadText>
<div className="pt-4">
<a
href="/contact"
className="inline-block text-3xl md:text-5xl font-bold text-slate-900 hover:text-slate-400 transition-all duration-700 border-b-2 border-slate-900 hover:border-slate-200 pb-2"
>
Projekt anfragen
</a>
</div>
</div>
<div className="w-full md:w-72 space-y-6 p-6 bg-slate-50 rounded-2xl border border-slate-100">
<div className="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-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>
);
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import Link from 'next/link';
import { blogPosts } from '../../../src/data/blogPosts';
import { MediumCard } from '../../../src/components/MediumCard';
export async function generateStaticParams() {
const allTags = Array.from(new Set(blogPosts.flatMap(post => post.tags || [])));
return allTags.map(tag => ({
tag,
}));
}
export default async function TagPage({ params }: { params: Promise<{ tag: string }> }) {
const { tag } = await params;
const posts = blogPosts.filter(post => post.tags?.includes(tag));
return (
<div className="max-w-3xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-3xl font-bold text-slate-900 mb-2">
Posts tagged <span className="highlighter-yellow px-2 rounded">{tag}</span>
</h1>
<p className="text-slate-600">
{posts.length} post{posts.length === 1 ? '' : 's'}
</p>
</header>
<div className="space-y-4">
{posts.map(post => (
<MediumCard key={post.slug} post={post} />
))}
</div>
<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>
</div>
);
}

View File

@@ -0,0 +1,99 @@
'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';
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">
<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>
<div className="flex flex-col md:flex-row items-start gap-8">
<div className={`p-6 rounded-2xl shadow-lg ${tech.color}`}>
<Icon className="w-12 h-12" />
</div>
<div className="flex-1">
<Label className="text-slate-400 mb-2">TECHNOLOGY DEEP DIVE</Label>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">{tech.title}</h1>
<p className="text-xl text-slate-500 font-medium">{tech.subtitle}</p>
</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>
<h2 className="text-2xl font-bold mb-4">What is it?</h2>
<p className="text-xl leading-relaxed text-slate-700">{tech.description}</p>
</section>
<section>
<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>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tech.benefits.map((benefit, i) => (
<div key={i} className="flex gap-3 p-4 bg-slate-50 rounded-xl border border-slate-100">
<Check className="w-5 h-5 text-green-600 shrink-0" />
<span className="font-medium text-slate-700">{benefit}</span>
</div>
))}
</div>
</section>
<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>
</div>
<div className="lg:col-span-4 space-y-8">
<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>
</div>
</div>
</Container>
</div>
);
}

View File

@@ -0,0 +1,105 @@
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' }
]
}
};

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { technologies } from './data';
import TechnologyContent from './content';
export default function TechnologyPage({ params }: { params: { slug: string } }) {
return <TechnologyContent slug={params.slug} />;
}
// Generate static params for these dynamic routes
export async function generateStaticParams() {
return Object.keys(technologies).map((slug) => ({
slug,
}));
}

View File

@@ -0,0 +1,309 @@
'use client';
import { PageHeader } from '../../src/components/PageHeader';
import { Reveal } from '../../src/components/Reveal';
import { Section } from '../../src/components/Section';
import {
SystemArchitecture,
SpeedPerformance,
SolidFoundation,
LayerSeparation,
DirectService,
TaskDone,
ConceptAutomation,
ConceptCode,
ConceptCommunication,
ConceptPrototyping,
ConceptSystem,
ConceptTarget
} from '../../src/components/Landing';
import { Check } from 'lucide-react';
import { H2, H3, H4, LeadText, BodyText, Label } from '../../src/components/Typography';
import { BackgroundGrid, Card } from '../../src/components/Layout';
import { MotionButton } from '../../src/components/Button';
export default function WebsitesPage() {
return (
<div className="flex flex-col bg-white overflow-hidden relative">
<BackgroundGrid />
<PageHeader
title={<>Websites, die <br /><span className="text-slate-200">einfach funktionieren.</span></>}
description="Keine Baukästen, keine Plugins, kein Overhead. Nur sauberer Code und maximale Performance."
backLink={{ href: '/', label: 'Zurück' }}
backgroundSymbol="W"
/>
{/* Intro / Problem */}
<Section
number="01"
title="Der Ansatz"
borderTop
illustration={<SystemArchitecture className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Ich baue Websites wie Systeme <br />
<span className="text-slate-200">nicht wie Broschüren.</span>
</H3>
</Reveal>
<Reveal delay={0.2}>
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
Eine Website ist kein Flyer. Sie ist ein <span className="text-slate-900">Werkzeug</span>, das jeden Tag arbeitet.
Deshalb baue ich sie stabil, schnell und wartungsfrei.
</LeadText>
</Reveal>
<Reveal delay={0.4}>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 pt-8">
{[
{ label: 'Stabil', icon: ConceptSystem },
{ label: 'Schnell', icon: ConceptAutomation },
{ label: 'Wartungsfrei', icon: ConceptCode },
{ label: 'Sicher', icon: ConceptTarget },
].map((item, i) => (
<div key={i} className="flex flex-col gap-3 group">
<div className="w-12 h-12 rounded-xl bg-slate-50 flex items-center justify-center border border-slate-100 group-hover:scale-110 transition-transform duration-500">
<item.icon className="w-6 h-6" />
</div>
<Label className="text-slate-900">{item.label}</Label>
</div>
))}
</div>
</Reveal>
</div>
</Section>
{/* Speed */}
<Section
number="02"
title="Performance"
borderTop
variant="gray"
illustration={<SpeedPerformance className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Geschwindigkeit ist <br />
<span className="text-slate-200">kein Extra. Sie ist Standard.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
<div className="md:col-span-7 space-y-8">
<Reveal delay={0.2}>
<LeadText className="text-xl text-slate-400">
Viele Websites sind langsam, weil sie zusammengeklickt sind. Meine sind schnell, weil sie <span className="text-slate-900">von Grund auf</span> entwickelt wurden.
</LeadText>
</Reveal>
<Reveal delay={0.4}>
<ul className="space-y-4">
{[
'Seiten laden ohne Verzögerung',
'Optimiert für Suchmaschinen (SEO)',
'Bessere Nutzererfahrung',
'Höhere Conversion-Rates',
].map((item, i) => (
<li key={i} className="flex items-center gap-4 group">
<div className="w-1.5 h-1.5 bg-slate-900 rounded-full group-hover:scale-150 transition-transform" />
<LeadText className="text-lg md:text-xl">{item}</LeadText>
</li>
))}
</ul>
</Reveal>
</div>
<div className="md:col-span-5">
<Reveal delay={0.6}>
<Card variant="white" padding="normal" className="text-center group">
<div className="text-7xl md:text-8xl font-bold text-slate-900 tracking-tighter group-hover:scale-110 transition-transform duration-700">90+</div>
<Label className="mt-4">Pagespeed Score</Label>
</Card>
</Reveal>
</div>
</div>
</div>
</Section>
{/* No Maintenance */}
<Section
number="03"
title="Technik"
borderTop
illustration={<SolidFoundation className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Keine Plugins. <br />
<span className="text-slate-200">Keine Abhängigkeiten.</span>
</H3>
</Reveal>
<Reveal delay={0.2}>
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
Ich nutze keine Baukästen, die sich selbst zerstören.
Ihre Website besteht aus <span className="text-slate-900">sauberem Code</span>, der Ihnen gehört.
</LeadText>
</Reveal>
<Reveal delay={0.4}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
<Card variant="white" padding="normal" className="group">
<div className="space-y-4">
<Label className="text-slate-900 mb-2">Code Qualität</Label>
<H4 className="text-2xl">Langlebigkeit</H4>
<BodyText>Modernste Web-Technologien für maximale Performance und Wartbarkeit.</BodyText>
</div>
</Card>
<Card variant="white" padding="normal" className="group">
<div className="space-y-4">
<Label className="text-slate-900 mb-2">Sicherheit</Label>
<H4 className="text-2xl">Resilienz</H4>
<BodyText>Minimale Angriffsfläche durch Verzicht auf unnötige Drittanbieter-Software.</BodyText>
</div>
</Card>
</div>
</Reveal>
</div>
</Section>
{/* Content/Tech Separation */}
<Section
number="04"
title="Inhalte"
borderTop
variant="gray"
illustration={<LayerSeparation className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Inhalte pflegen <br />
<span className="text-slate-200">ohne Angst.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 items-start">
<div className="md:col-span-7">
<Reveal delay={0.2}>
<LeadText className="text-xl md:text-2xl text-slate-400">
Sie können Texte und Bilder selbst anpassen, ohne das Design oder die Technik zu gefährden.
Ein <span className="text-slate-900">intuitives System</span> sorgt dafür, dass alles an seinem Platz bleibt.
</LeadText>
</Reveal>
</div>
<div className="md:col-span-5">
<Reveal delay={0.4}>
<Card variant="white" padding="normal" className="space-y-8">
<div className="space-y-4">
<Label>Ihre Freiheit</Label>
<div className="flex items-center gap-3 text-xl font-bold text-slate-900">
<div className="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
Inhalte flexibel verwalten
</div>
</div>
<div className="space-y-4 opacity-30">
<Label>Mein Schutz</Label>
<div className="space-y-2">
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">Design-Chaos</div>
<div className="flex items-center gap-3 text-xl font-bold text-slate-900 line-through">Technische Fehler</div>
</div>
</div>
</Card>
</Reveal>
</div>
</div>
</div>
</Section>
{/* Simple Changes */}
<Section
number="05"
title="Service"
borderTop
illustration={<DirectService className="w-24 h-24" />}
>
<div className="space-y-12">
<Reveal>
<H3 className="text-3xl md:text-5xl leading-tight max-w-3xl">
Änderungen sind <br />
<span className="text-slate-200">Teil des Konzepts.</span>
</H3>
</Reveal>
<Reveal delay={0.2}>
<LeadText className="text-xl md:text-2xl max-w-2xl text-slate-400">
Ihr Business entwickelt sich weiter, Ihre Website auch. <br />
Keine komplizierten Prozesse, sondern <span className="text-slate-900">direkte Umsetzung</span> Ihrer Ideen.
</LeadText>
</Reveal>
<Reveal delay={0.4}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-8">
<Card variant="white" padding="normal" className="group hover:border-slate-900">
<ConceptCommunication className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
<div className="space-y-2">
<H4 className="text-2xl">Direkter Draht</H4>
<BodyText>Sie sprechen direkt mit dem Entwickler. Keine Stille Post.</BodyText>
</div>
</Card>
<Card variant="white" padding="normal" className="group hover:border-slate-900">
<ConceptPrototyping className="w-12 h-12 mb-8 group-hover:scale-110 transition-all duration-700" />
<div className="space-y-2">
<H4 className="text-2xl">Agile Anpassung</H4>
<BodyText>Schnelle Iterationen statt langer Wartezeiten.</BodyText>
</div>
</Card>
</div>
</Reveal>
</div>
</Section>
{/* Result */}
<Section
number="06"
title="Ergebnis"
borderTop
variant="gray"
illustration={<TaskDone className="w-24 h-24" />}
>
<div className="space-y-16">
<Reveal>
<H3 className="text-4xl md:text-6xl tracking-tighter">
Eine Website, die <br />
<span className="text-slate-200">einfach läuft.</span>
</H3>
</Reveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{[
{ title: 'Kein Overhead', desc: 'Fokus auf das, was Ihre Kunden wirklich brauchen.' },
{ title: 'Volle Kontrolle', desc: 'Der Code gehört Ihnen, ohne Vendor Lock-in.' },
{ title: 'Echte Performance', desc: 'Messbare Geschwindigkeit für bessere Ergebnisse.' },
].map((item, i) => (
<Reveal key={i} delay={i * 0.1}>
<div className="space-y-4 group">
<div className="w-8 h-px bg-slate-200 group-hover:w-full transition-all duration-1000" />
<H4 className="text-2xl">{item.title}</H4>
<LeadText className="text-lg text-slate-400">{item.desc}</LeadText>
</div>
</Reveal>
))}
</div>
<Reveal delay={0.4}>
<div className="pt-16 border-t border-slate-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-8">
<div className="space-y-2">
<Label>Bereit für mehr?</Label>
<LeadText className="text-2xl">
Lassen Sie uns über Ihr nächstes Projekt sprechen.
</LeadText>
</div>
<MotionButton href="/contact">
Projekt anfragen
</MotionButton>
</div>
</Reveal>
</div>
</Section>
</div>
);
}