Initial import: was v0 exportiert hat

This commit is contained in:
Dieter Schlüter 2026-04-24 17:12:51 +02:00
commit 0194931215
167 changed files with 16465 additions and 0 deletions

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
# v0 sandbox internal files
__v0_runtime_loader.js
__v0_devtools.tsx
__v0_jsx-dev-runtime.ts
.snowflake/
.v0-trash/
.vercel/
next.user-config.*
# Environment variables
.env*.local
# Common ignores
node_modules
.next/
.DS_Store

File diff suppressed because one or more lines are too long

73
app/datenschutz/page.tsx Normal file
View file

@ -0,0 +1,73 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import { Button } from '@/components/ui/button'
export const metadata = {
title: 'Datenschutz | Jamulix',
description: 'Datenschutzerklärung für jamulix.de',
}
export default function DatenschutzPage() {
return (
<main className="min-h-screen py-24 lg:py-32">
<div className="max-w-3xl mx-auto px-6 lg:px-8">
<Button asChild variant="ghost" className="mb-8 -ml-4">
<Link href="/">
<ArrowLeft className="mr-2 size-4" />
Zurück
</Link>
</Button>
<h1 className="font-serif text-4xl lg:text-5xl mb-8">Datenschutz</h1>
<div className="prose prose-neutral dark:prose-invert max-w-none space-y-8">
<section>
<h2 className="font-serif text-xl mb-4">1. Datenschutz auf einen Blick</h2>
<h3 className="font-medium mt-6 mb-2">Allgemeine Hinweise</h3>
<p className="text-muted-foreground leading-relaxed">
Die folgenden Hinweise geben einen einfachen Überblick darüber,
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese
Website besuchen.
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">2. Hosting</h2>
<p className="text-muted-foreground leading-relaxed">
Diese Website wird bei Vercel gehostet. Der Anbieter ist
Vercel Inc., 340 S Lemon Ave #4133, Walnut, CA 91789, USA.
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">3. Allgemeine Hinweise</h2>
<p className="text-muted-foreground leading-relaxed">
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen
Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten
vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften
sowie dieser Datenschutzerklärung.
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">4. Datenerfassung auf dieser Website</h2>
<h3 className="font-medium mt-6 mb-2">Server-Log-Dateien</h3>
<p className="text-muted-foreground leading-relaxed">
Der Provider dieser Seiten erhebt und speichert automatisch
Informationen in so genannten Server-Log-Dateien, die Ihr Browser
automatisch an uns übermittelt.
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">5. Kontakt</h2>
<p className="text-muted-foreground leading-relaxed">
Bei Fragen zum Datenschutz können Sie sich jederzeit an uns wenden:<br />
E-Mail: kontakt@jamulix.de
</p>
</section>
</div>
</div>
</main>
)
}

114
app/globals.css Normal file
View file

@ -0,0 +1,114 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
/* Light mode - warm paper tones */
--background: oklch(0.98 0.005 90);
--foreground: oklch(0.18 0.01 270);
--card: oklch(0.96 0.005 90);
--card-foreground: oklch(0.18 0.01 270);
--popover: oklch(0.98 0.005 90);
--popover-foreground: oklch(0.18 0.01 270);
--primary: oklch(0.18 0.01 270);
--primary-foreground: oklch(0.98 0.005 90);
--secondary: oklch(0.94 0.005 90);
--secondary-foreground: oklch(0.25 0.01 270);
--muted: oklch(0.92 0.005 90);
--muted-foreground: oklch(0.45 0.01 270);
--accent: oklch(0.65 0.15 145);
--accent-foreground: oklch(0.15 0.02 145);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.88 0.005 90);
--input: oklch(0.92 0.005 90);
--ring: oklch(0.65 0.15 145);
--radius: 0.5rem;
}
.dark {
/* Dark mode - deep charcoal with warmth */
--background: oklch(0.13 0.01 270);
--foreground: oklch(0.92 0.01 90);
--card: oklch(0.16 0.01 270);
--card-foreground: oklch(0.92 0.01 90);
--popover: oklch(0.16 0.01 270);
--popover-foreground: oklch(0.92 0.01 90);
--primary: oklch(0.92 0.01 90);
--primary-foreground: oklch(0.13 0.01 270);
--secondary: oklch(0.22 0.01 270);
--secondary-foreground: oklch(0.85 0.01 90);
--muted: oklch(0.20 0.01 270);
--muted-foreground: oklch(0.60 0.01 90);
--accent: oklch(0.72 0.14 140);
--accent-foreground: oklch(0.13 0.01 270);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.25 0.01 270);
--input: oklch(0.22 0.01 270);
--ring: oklch(0.72 0.14 140);
}
@theme inline {
--font-sans: 'Inter', 'Inter Fallback', system-ui, sans-serif;
--font-serif: 'Instrument Serif', 'Georgia', serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}
/* Selection colors */
::selection {
background: oklch(0.72 0.14 140 / 0.3);
}
.dark ::selection {
background: oklch(0.72 0.14 140 / 0.4);
}

61
app/impressum/page.tsx Normal file
View file

@ -0,0 +1,61 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import { Button } from '@/components/ui/button'
export const metadata = {
title: 'Impressum | Jamulix',
description: 'Impressum und Angaben gemäß § 5 TMG für jamulix.de',
}
export default function ImpressumPage() {
return (
<main className="min-h-screen py-24 lg:py-32">
<div className="max-w-3xl mx-auto px-6 lg:px-8">
<Button asChild variant="ghost" className="mb-8 -ml-4">
<Link href="/">
<ArrowLeft className="mr-2 size-4" />
Zurück
</Link>
</Button>
<h1 className="font-serif text-4xl lg:text-5xl mb-8">Impressum</h1>
<div className="prose prose-neutral dark:prose-invert max-w-none space-y-8">
<section>
<h2 className="font-serif text-xl mb-4">Angaben gemäß § 5 TMG</h2>
<p className="text-muted-foreground">
[Name]<br />
[Straße und Hausnummer]<br />
[PLZ Ort]<br />
Deutschland
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">Kontakt</h2>
<p className="text-muted-foreground">
E-Mail: kontakt@jamulix.de
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">Verantwortlich für den Inhalt</h2>
<p className="text-muted-foreground">
[Name]<br />
[Adresse]
</p>
</section>
<section>
<h2 className="font-serif text-xl mb-4">Haftungsausschluss</h2>
<p className="text-muted-foreground leading-relaxed">
Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt.
Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte
kann jedoch keine Gewähr übernommen werden.
</p>
</section>
</div>
</div>
</main>
)
}

90
app/layout.tsx Normal file
View file

@ -0,0 +1,90 @@
import type { Metadata, Viewport } from 'next'
import { Inter, Instrument_Serif, JetBrains_Mono } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next'
import './globals.css'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const instrumentSerif = Instrument_Serif({
weight: '400',
subsets: ['latin'],
variable: '--font-instrument-serif',
display: 'swap',
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-jetbrains-mono',
display: 'swap',
})
export const metadata: Metadata = {
title: 'Jamulix | 40 Jahre Code',
description: 'Seit 40 Jahren Code. Von Fortran IV bis KI-Projekte in Python und Rust. Erfahrener Programmierer, Linux-Veteran und KI-Entwickler.',
keywords: ['Programmierung', 'Linux', 'KI', 'Python', 'Rust', 'Software-Entwicklung', 'AI', 'Fortran', 'Unix'],
authors: [{ name: 'Jamulix' }],
creator: 'Jamulix',
openGraph: {
type: 'website',
locale: 'de_DE',
url: 'https://jamulix.de',
siteName: 'Jamulix',
title: 'Jamulix | 40 Jahre Code',
description: 'Seit 40 Jahren Code. Von Fortran IV bis KI-Projekte in Python und Rust.',
},
twitter: {
card: 'summary_large_image',
title: 'Jamulix | 40 Jahre Code',
description: 'Seit 40 Jahren Code. Von Fortran IV bis KI-Projekte in Python und Rust.',
},
robots: {
index: true,
follow: true,
},
}
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#faf9f7' },
{ media: '(prefers-color-scheme: dark)', color: '#1a1a1f' },
],
width: 'device-width',
initialScale: 1,
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html
lang="de"
className={`${inter.variable} ${instrumentSerif.variable} ${jetbrainsMono.variable} dark bg-background`}
suppressHydrationWarning
>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
})();
`,
}}
/>
</head>
<body className="font-sans antialiased min-h-screen">
{children}
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
)
}

23
app/page.tsx Normal file
View file

@ -0,0 +1,23 @@
import { Header } from '@/components/header'
import { Hero } from '@/components/hero'
import { AboutSection } from '@/components/about-section'
import { ProjectsSection } from '@/components/projects-section'
import { FocusSection } from '@/components/focus-section'
import { PhilosophySection } from '@/components/philosophy-section'
import { Footer } from '@/components/footer'
export default function Home() {
return (
<>
<Header />
<main>
<Hero />
<AboutSection />
<ProjectsSection />
<FocusSection />
<PhilosophySection />
</main>
<Footer />
</>
)
}

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -0,0 +1,120 @@
'use client'
import { motion } from 'framer-motion'
const timeline = [
{
era: '1984',
title: 'Der Anfang',
description: 'Erste Programme in Fortran IV. Lochkarten, Großrechner, Batch-Verarbeitung. Die Grundlagen des algorithmischen Denkens.',
tech: ['Fortran IV'],
},
{
era: '1986',
title: 'Systemnahe Programmierung',
description: 'C und 68000-Assembler auf dem Atari ST. Direkte Hardware-Ansteuerung, Interrupt-Routinen, Speicherverwaltung von Hand.',
tech: ['C', '68000 ASM', 'GFA Basic'],
},
{
era: '1990',
title: 'Unix an der Universität',
description: 'SunOS, HP-UX, BSD. Die Philosophie kleiner, zusammenarbeitender Programme. Shell-Scripting als Werkzeug.',
tech: ['Unix', 'Shell', 'vi'],
},
{
era: '1994',
title: 'Linux entdeckt',
description: 'Slackware auf 40 Disketten. Der Beginn einer dauerhaften Begeisterung für offene Systeme und Selbstbestimmung.',
tech: ['Linux', 'GNU Tools', 'X11'],
},
{
era: '2000er',
title: 'Professionelle Entwicklung',
description: 'Verschiedene Sprachen, verschiedene Domänen. Server-Administration, Automatisierung, Web-Entwicklung.',
tech: ['Perl', 'PHP', 'Bash', 'Python'],
},
{
era: '2022',
title: 'KI-Fokus',
description: 'Seit Ende 2022 intensive Beschäftigung mit KI-Tools und KI-Programmierung. LLMs als Arbeitswerkzeug und Forschungsgegenstand.',
tech: ['Python', 'Rust', 'LLMs', 'ML'],
},
]
export function AboutSection() {
return (
<section id="ueber" className="py-24 lg:py-32">
<div className="max-w-6xl mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="mb-16 lg:mb-24"
>
<span className="font-mono text-xs tracking-wider text-accent uppercase">
Hintergrund
</span>
<h2 className="font-serif text-3xl sm:text-4xl lg:text-5xl mt-4 text-balance">
Vier Jahrzehnte Programmierung
</h2>
<p className="mt-6 text-lg text-muted-foreground max-w-2xl">
Eine Reise durch die Evolution der Software-Entwicklung –
von Lochkarten bis zu Large Language Models.
</p>
</motion.div>
{/* Timeline */}
<div className="relative">
{/* Vertical line */}
<div className="absolute left-0 lg:left-1/2 top-0 bottom-0 w-px bg-border lg:-translate-x-px hidden sm:block" />
<div className="space-y-12 lg:space-y-16">
{timeline.map((item, index) => (
<motion.div
key={item.era}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={`relative grid lg:grid-cols-2 gap-8 lg:gap-16 ${
index % 2 === 0 ? '' : 'lg:direction-rtl'
}`}
>
{/* Timeline dot */}
<div className="absolute left-0 lg:left-1/2 top-0 -translate-x-1/2 hidden sm:block">
<div className="size-3 rounded-full bg-background border-2 border-accent" />
</div>
{/* Content */}
<div className={`pl-8 sm:pl-12 lg:pl-0 ${index % 2 === 0 ? 'lg:pr-16 lg:text-right' : 'lg:col-start-2 lg:pl-16'}`}>
<div className={`lg:direction-ltr ${index % 2 === 0 ? 'lg:ml-auto lg:max-w-md' : 'lg:max-w-md'}`}>
<span className="font-mono text-sm text-accent">{item.era}</span>
<h3 className="font-serif text-xl lg:text-2xl mt-2">{item.title}</h3>
<p className="mt-3 text-muted-foreground leading-relaxed">
{item.description}
</p>
<div className={`mt-4 flex flex-wrap gap-2 ${index % 2 === 0 ? 'lg:justify-end' : ''}`}>
{item.tech.map((tech) => (
<span
key={tech}
className="font-mono text-xs px-2 py-1 bg-secondary text-secondary-foreground rounded"
>
{tech}
</span>
))}
</div>
</div>
</div>
{/* Spacer for alternating layout */}
{index % 2 !== 0 && <div className="hidden lg:block" />}
</motion.div>
))}
</div>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,122 @@
'use client'
import { motion } from 'framer-motion'
const focusAreas = [
{
category: 'Sprachen',
items: [
{ name: 'Python', detail: 'Haupt-Sprache für KI und Automatisierung' },
{ name: 'Rust', detail: 'Für performante Systemtools' },
{ name: 'Shell/Bash', detail: 'Tägliches Werkzeug' },
],
},
{
category: 'Systeme',
items: [
{ name: 'Linux', detail: 'Arch, Debian, NixOS' },
{ name: 'Self-Hosting', detail: 'Eigene Infrastruktur' },
{ name: 'Containers', detail: 'Docker, Podman' },
],
},
{
category: 'KI & ML',
items: [
{ name: 'LLMs', detail: 'GPT, Claude, lokale Modelle' },
{ name: 'KI-Tools', detail: 'Cursor, Copilot, Aider' },
{ name: 'ML Ops', detail: 'Training, Deployment' },
],
},
{
category: 'Interessen',
items: [
{ name: 'Automatisierung', detail: 'Workflows, Pipelines' },
{ name: 'CLI Tools', detail: 'Terminal-first' },
{ name: 'Open Source', detail: 'Nutzung & Beteiligung' },
],
},
]
export function FocusSection() {
return (
<section id="fokus" className="py-24 lg:py-32">
<div className="max-w-6xl mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="mb-16"
>
<span className="font-mono text-xs tracking-wider text-accent uppercase">
Aktuell
</span>
<h2 className="font-serif text-3xl sm:text-4xl lg:text-5xl mt-4 text-balance">
Technologien & Fokus
</h2>
<p className="mt-6 text-lg text-muted-foreground max-w-2xl">
Die Werkzeuge und Themen, mit denen ich aktuell arbeite.
Kein Buzzword-Bingo, sondern tägliche Praxis.
</p>
</motion.div>
{/* Focus Grid */}
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-6">
{focusAreas.map((area, areaIndex) => (
<motion.div
key={area.category}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: areaIndex * 0.1 }}
>
<h3 className="font-mono text-sm text-accent mb-6 pb-3 border-b border-border">
{area.category}
</h3>
<ul className="space-y-4">
{area.items.map((item, itemIndex) => (
<motion.li
key={item.name}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: areaIndex * 0.1 + itemIndex * 0.05 }}
className="group"
>
<span className="block font-medium text-foreground group-hover:text-accent transition-colors">
{item.name}
</span>
<span className="block text-sm text-muted-foreground mt-0.5">
{item.detail}
</span>
</motion.li>
))}
</ul>
</motion.div>
))}
</div>
{/* Secondary visual element */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.4 }}
className="mt-20 pt-12 border-t border-border"
>
<div className="flex flex-wrap gap-3">
{['vim', 'git', 'tmux', 'ssh', 'systemd', 'nginx', 'PostgreSQL', 'Redis', 'FastAPI', 'PyTorch', 'Transformers', 'tiktoken'].map((tool) => (
<span
key={tool}
className="font-mono text-xs px-3 py-1.5 border border-border rounded text-muted-foreground hover:text-foreground hover:border-accent/50 transition-colors cursor-default"
>
{tool}
</span>
))}
</div>
</motion.div>
</div>
</section>
)
}

128
components/footer.tsx Normal file
View file

@ -0,0 +1,128 @@
'use client'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { Github, Mail, ArrowUpRight } from 'lucide-react'
const footerLinks = [
{ label: 'Projekte', href: '#projekte' },
{ label: 'Über', href: '#ueber' },
{ label: 'Fokus', href: '#fokus' },
{ label: 'Philosophie', href: '#philosophie' },
]
const legalLinks = [
{ label: 'Impressum', href: '/impressum' },
{ label: 'Datenschutz', href: '/datenschutz' },
]
const socialLinks = [
{ label: 'GitHub', href: 'https://github.com', icon: Github },
{ label: 'E-Mail', href: 'mailto:kontakt@jamulix.de', icon: Mail },
]
export function Footer() {
const currentYear = new Date().getFullYear()
return (
<footer id="kontakt" className="py-16 lg:py-20 border-t border-border">
<div className="max-w-6xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{/* Main footer content */}
<div className="grid lg:grid-cols-12 gap-12 lg:gap-8">
{/* Brand section */}
<div className="lg:col-span-5">
<Link
href="/"
className="inline-block font-mono text-xl tracking-tight text-foreground hover:text-accent transition-colors"
>
Jamulix
</Link>
<p className="mt-4 text-muted-foreground max-w-sm leading-relaxed">
40 Jahre Programmierung. Von Fortran bis KI.
Linux-Enthusiast seit 1994. Aktuell Python und Rust.
</p>
{/* Social links */}
<div className="flex gap-4 mt-6">
{socialLinks.map((link) => {
const Icon = link.icon
return (
<Link
key={link.label}
href={link.href}
target={link.href.startsWith('http') ? '_blank' : undefined}
rel={link.href.startsWith('http') ? 'noopener noreferrer' : undefined}
className="group flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
aria-label={link.label}
>
<Icon className="size-5" />
<span className="text-sm">{link.label}</span>
{link.href.startsWith('http') && (
<ArrowUpRight className="size-3 opacity-0 group-hover:opacity-100 transition-opacity" />
)}
</Link>
)
})}
</div>
</div>
{/* Navigation */}
<div className="lg:col-span-3 lg:col-start-7">
<h3 className="font-mono text-sm text-accent mb-4">Navigation</h3>
<ul className="space-y-3">
{footerLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Legal */}
<div className="lg:col-span-3">
<h3 className="font-mono text-sm text-accent mb-4">Rechtliches</h3>
<ul className="space-y-3">
{legalLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</Link>
</li>
))}
<li>
<span className="text-muted-foreground/60 text-sm">
Blog (bald)
</span>
</li>
</ul>
</div>
</div>
{/* Bottom bar */}
<div className="mt-16 pt-8 border-t border-border flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-sm text-muted-foreground">
&copy; {currentYear} Jamulix. Alle Rechte vorbehalten.
</p>
<p className="font-mono text-xs text-muted-foreground/60">
Gebaut mit Next.js, Tailwind, und viel Kaffee.
</p>
</div>
</motion.div>
</div>
</footer>
)
}

98
components/header.tsx Normal file
View file

@ -0,0 +1,98 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Menu, X } from 'lucide-react'
import { ThemeToggle } from './theme-toggle'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const navLinks = [
{ href: '#projekte', label: 'Projekte' },
{ href: '#ueber', label: 'Über' },
{ href: '#fokus', label: 'Fokus' },
{ href: '#philosophie', label: 'Philosophie' },
{ href: '#kontakt', label: 'Kontakt' },
]
export function Header() {
const [isScrolled, setIsScrolled] = useState(false)
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20)
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return (
<header
className={cn(
'fixed top-0 left-0 right-0 z-50 transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b border-border/50'
: 'bg-transparent'
)}
>
<div className="max-w-6xl mx-auto px-6 lg:px-8">
<nav className="flex items-center justify-between h-16 lg:h-20">
{/* Logo */}
<Link
href="/"
className="font-mono text-lg tracking-tight text-foreground hover:text-accent transition-colors"
>
Jamulix
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-8">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</Link>
))}
<ThemeToggle />
</div>
{/* Mobile Menu Button */}
<div className="flex items-center gap-2 md:hidden">
<ThemeToggle />
<Button
variant="ghost"
size="icon"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="size-9 text-muted-foreground"
aria-label={isMobileMenuOpen ? 'Menü schließen' : 'Menü öffnen'}
>
{isMobileMenuOpen ? <X className="size-5" /> : <Menu className="size-5" />}
</Button>
</div>
</nav>
{/* Mobile Navigation */}
{isMobileMenuOpen && (
<div className="md:hidden pb-6 border-b border-border/50">
<div className="flex flex-col gap-4">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setIsMobileMenuOpen(false)}
className="text-sm text-muted-foreground hover:text-foreground transition-colors py-2"
>
{link.label}
</Link>
))}
</div>
</div>
)}
</div>
</header>
)
}

108
components/hero.tsx Normal file
View file

@ -0,0 +1,108 @@
'use client'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { ArrowDown, ArrowRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { SystemDiagram } from './system-diagram'
export function Hero() {
return (
<section className="relative min-h-screen flex items-center pt-20 lg:pt-0">
{/* Subtle grid background */}
<div className="absolute inset-0 overflow-hidden">
<div
className="absolute inset-0 opacity-[0.02] dark:opacity-[0.04]"
style={{
backgroundImage: `
linear-gradient(to right, currentColor 1px, transparent 1px),
linear-gradient(to bottom, currentColor 1px, transparent 1px)
`,
backgroundSize: '60px 60px',
}}
/>
</div>
<div className="relative max-w-6xl mx-auto px-6 lg:px-8 py-20 lg:py-32">
<div className="grid lg:grid-cols-12 gap-12 lg:gap-16 items-center">
{/* Text Content */}
<div className="lg:col-span-7 space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<span className="inline-block font-mono text-xs tracking-wider text-accent uppercase mb-6">
1984 – heute
</span>
<h1 className="font-serif text-4xl sm:text-5xl lg:text-6xl xl:text-7xl leading-[1.1] tracking-tight text-balance">
Seit 40 Jahren Code. Von Fortran{'\u00A0'}IV bis KI-Projekte in Python und Rust.
</h1>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-lg lg:text-xl text-muted-foreground leading-relaxed max-w-2xl"
>
Erste Programmiersprache: Fortran{'\u00A0'}IV. Dann C und 68000-Assembler auf dem Atari{'\u00A0'}ST.
An der Universität Unix entdeckt – und Linux nie wieder losgelassen.
Seit Ende 2022 konzentriert auf KI-Tools und KI-Programmierung.
Heute: Python und Rust.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex flex-col sm:flex-row gap-4 pt-4"
>
<Button asChild size="lg" className="group">
<Link href="#projekte">
Projekte ansehen
<ArrowDown className="ml-2 size-4 transition-transform group-hover:translate-y-0.5" />
</Link>
</Button>
<Button asChild variant="outline" size="lg" className="group">
<Link href="#ueber">
Über Jamulix
<ArrowRight className="ml-2 size-4 transition-transform group-hover:translate-x-0.5" />
</Link>
</Button>
</motion.div>
</div>
{/* System Diagram */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="lg:col-span-5 hidden lg:block"
>
<SystemDiagram />
</motion.div>
</div>
{/* Scroll indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.8 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2 hidden lg:block"
>
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<span className="font-mono text-xs tracking-wider">Scroll</span>
<motion.div
animate={{ y: [0, 6, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
>
<ArrowDown className="size-4" />
</motion.div>
</div>
</motion.div>
</div>
</section>
)
}

View file

@ -0,0 +1,88 @@
'use client'
import { motion } from 'framer-motion'
const principles = [
{
title: 'Langfristige Neugier',
text: 'Technologie verändert sich ständig. Das Interesse daran nicht. Wer 1984 mit Programmieren begonnen hat, hat viele Paradigmenwechsel erlebt – und überlebt.',
},
{
title: 'Handwerkliche Sorgfalt',
text: 'Code ist keine Einwegware. Lesbarkeit, Wartbarkeit, Einfachheit – das sind keine Nice-to-haves, sondern Grundanforderungen.',
},
{
title: 'Systeme verstehen',
text: 'Nicht nur nutzen, sondern begreifen. Vom Kernel bis zum Compiler. Nur wer versteht, wie Dinge zusammenhängen, kann sie wirklich beherrschen.',
},
{
title: 'KI als Werkzeug',
text: 'Künstliche Intelligenz ist kein Hype, sondern ein ernsthaftes Werkzeug. Es verändert, wie wir programmieren – aber nicht warum.',
},
]
export function PhilosophySection() {
return (
<section id="philosophie" className="py-24 lg:py-32 bg-secondary/30">
<div className="max-w-6xl mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="mb-16 lg:mb-20"
>
<span className="font-mono text-xs tracking-wider text-accent uppercase">
Prinzipien
</span>
<h2 className="font-serif text-3xl sm:text-4xl lg:text-5xl mt-4 text-balance">
Philosophie
</h2>
</motion.div>
{/* Principles Grid */}
<div className="grid lg:grid-cols-2 gap-12 lg:gap-x-16 lg:gap-y-14">
{principles.map((principle, index) => (
<motion.article
key={principle.title}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="relative"
>
{/* Decorative number */}
<span className="absolute -left-2 lg:-left-8 top-0 font-mono text-6xl lg:text-7xl font-bold text-border/50 select-none">
{String(index + 1).padStart(2, '0')}
</span>
<div className="relative pl-8 lg:pl-0">
<h3 className="font-serif text-xl lg:text-2xl mb-4">
{principle.title}
</h3>
<p className="text-muted-foreground leading-relaxed">
{principle.text}
</p>
</div>
</motion.article>
))}
</div>
{/* Closing statement */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
className="mt-20 pt-12 border-t border-border"
>
<blockquote className="font-serif text-xl lg:text-2xl text-center max-w-3xl mx-auto text-balance">
&ldquo;Der beste Code ist der, den man in zehn Jahren noch versteht –
geschrieben von jemandem, der in zehn Jahren noch lernt.&rdquo;
</blockquote>
</motion.div>
</div>
</section>
)
}

View file

@ -0,0 +1,154 @@
'use client'
import { motion } from 'framer-motion'
import { ArrowUpRight, ExternalLink } from 'lucide-react'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
const projects = [
{
title: 'Hacker News Reader',
slug: 'hackernews',
description: 'Ein minimalistischer, schneller Hacker News Client. Fokus auf Lesbarkeit und schnelles Laden.',
tech: ['Python', 'FastAPI', 'HTMX'],
status: 'Aktiv',
url: 'https://jamulix.de/hackernews/',
featured: true,
},
{
title: 'Sorting Visualizer',
slug: 'sorting',
description: 'Interaktive Visualisierung verschiedener Sortieralgorithmen. Didaktisches Werkzeug für Algorithmen-Verständnis.',
tech: ['Rust', 'WASM', 'Canvas'],
status: 'Aktiv',
url: 'https://jamulix.de/sorting',
featured: true,
},
{
title: 'Projekt in Arbeit',
slug: 'upcoming-1',
description: 'KI-gestütztes Werkzeug für Code-Analyse und Dokumentation. Details folgen.',
tech: ['Python', 'LLM', 'AST'],
status: 'In Entwicklung',
url: null,
featured: false,
},
{
title: 'Geplant',
slug: 'upcoming-2',
description: 'Self-hosted Infrastruktur-Monitoring. Lightweight, ohne Cloud-Abhängigkeiten.',
tech: ['Rust', 'Linux', 'SQLite'],
status: 'Geplant',
url: null,
featured: false,
},
]
function ProjectCard({ project, index }: { project: typeof projects[0]; index: number }) {
const isClickable = project.url !== null
return (
<motion.article
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={`group relative ${project.featured ? 'lg:col-span-1' : ''}`}
>
<div
className={`
relative h-full p-6 lg:p-8 rounded-lg border border-border
bg-card transition-all duration-300
${isClickable ? 'hover:border-accent/50 hover:bg-card/80' : 'opacity-70'}
`}
>
{/* Status badge */}
<div className="flex items-center justify-between mb-4">
<span
className={`
font-mono text-xs px-2 py-1 rounded
${project.status === 'Aktiv'
? 'bg-accent/10 text-accent'
: project.status === 'In Entwicklung'
? 'bg-secondary text-muted-foreground'
: 'bg-secondary/50 text-muted-foreground/70'
}
`}
>
{project.status}
</span>
{isClickable && (
<ArrowUpRight className="size-5 text-muted-foreground group-hover:text-accent transition-colors" />
)}
</div>
{/* Title */}
<h3 className="font-serif text-xl lg:text-2xl mb-3 group-hover:text-accent transition-colors">
{project.title}
</h3>
{/* Description */}
<p className="text-muted-foreground leading-relaxed mb-6">
{project.description}
</p>
{/* Tech stack */}
<div className="flex flex-wrap gap-2 mb-6">
{project.tech.map((tech) => (
<span
key={tech}
className="font-mono text-xs px-2 py-1 bg-secondary text-secondary-foreground rounded"
>
{tech}
</span>
))}
</div>
{/* Link */}
{isClickable && (
<Button asChild variant="outline" size="sm" className="group/btn">
<Link href={project.url} target="_blank" rel="noopener noreferrer">
Ansehen
<ExternalLink className="ml-2 size-3 transition-transform group-hover/btn:translate-x-0.5" />
</Link>
</Button>
)}
</div>
</motion.article>
)
}
export function ProjectsSection() {
return (
<section id="projekte" className="py-24 lg:py-32 bg-secondary/30">
<div className="max-w-6xl mx-auto px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="mb-16"
>
<span className="font-mono text-xs tracking-wider text-accent uppercase">
Experimente
</span>
<h2 className="font-serif text-3xl sm:text-4xl lg:text-5xl mt-4 text-balance">
Ausgewählte Projekte
</h2>
<p className="mt-6 text-lg text-muted-foreground max-w-2xl">
Technische Experimente und Werkzeuge. Weniger Portfolio,
mehr Labor für Ideen.
</p>
</motion.div>
{/* Projects Grid */}
<div className="grid md:grid-cols-2 gap-6 lg:gap-8">
{projects.map((project, index) => (
<ProjectCard key={project.slug} project={project} index={index} />
))}
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,119 @@
'use client'
import { motion } from 'framer-motion'
const nodes = [
{ id: 'fortran', label: 'Fortran IV', x: 20, y: 15, era: '1984' },
{ id: 'c', label: 'C', x: 45, y: 25, era: '1986' },
{ id: 'asm', label: '68K ASM', x: 75, y: 20, era: '1987' },
{ id: 'unix', label: 'Unix', x: 30, y: 45, era: '1990' },
{ id: 'linux', label: 'Linux', x: 60, y: 50, era: '1994' },
{ id: 'python', label: 'Python', x: 25, y: 75, era: '2020' },
{ id: 'rust', label: 'Rust', x: 55, y: 80, era: '2022' },
{ id: 'ai', label: 'KI', x: 80, y: 70, era: '2023' },
]
const connections = [
['fortran', 'c'],
['c', 'asm'],
['c', 'unix'],
['unix', 'linux'],
['linux', 'python'],
['linux', 'rust'],
['python', 'ai'],
['rust', 'ai'],
]
export function SystemDiagram() {
const getNodePosition = (id: string) => {
const node = nodes.find(n => n.id === id)
return node ? { x: node.x, y: node.y } : { x: 0, y: 0 }
}
return (
<div className="relative aspect-square w-full max-w-md mx-auto">
{/* SVG for connections */}
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.1" />
<stop offset="50%" stopColor="currentColor" stopOpacity="0.3" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0.1" />
</linearGradient>
</defs>
{connections.map(([from, to], index) => {
const start = getNodePosition(from)
const end = getNodePosition(to)
return (
<motion.line
key={`${from}-${to}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
stroke="url(#lineGradient)"
strokeWidth="0.5"
className="text-accent"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
duration: 0.8,
delay: 0.5 + index * 0.1,
ease: 'easeOut'
}}
/>
)
})}
</svg>
{/* Nodes */}
{nodes.map((node, index) => (
<motion.div
key={node.id}
className="absolute transform -translate-x-1/2 -translate-y-1/2"
style={{ left: `${node.x}%`, top: `${node.y}%` }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.4,
delay: 0.3 + index * 0.08,
type: 'spring',
stiffness: 200
}}
>
<div className="group relative">
{/* Node circle */}
<div className="relative">
<div className="size-3 rounded-full bg-accent/20 border border-accent/40 group-hover:bg-accent/30 transition-colors" />
<div className="absolute inset-0 size-3 rounded-full bg-accent/60 animate-ping opacity-0 group-hover:opacity-30" />
</div>
{/* Label */}
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-1.5 whitespace-nowrap">
<span className="font-mono text-[10px] text-muted-foreground group-hover:text-foreground transition-colors">
{node.label}
</span>
</div>
{/* Era tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<span className="font-mono text-[9px] text-accent bg-background/80 px-1.5 py-0.5 rounded border border-border/50">
{node.era}
</span>
</div>
</div>
</motion.div>
))}
{/* Decorative elements */}
<div className="absolute inset-0 pointer-events-none">
{/* Corner accents */}
<div className="absolute top-0 left-0 w-8 h-8 border-l border-t border-border/30" />
<div className="absolute top-0 right-0 w-8 h-8 border-r border-t border-border/30" />
<div className="absolute bottom-0 left-0 w-8 h-8 border-l border-b border-border/30" />
<div className="absolute bottom-0 right-0 w-8 h-8 border-r border-b border-border/30" />
</div>
</div>
)
}

View file

@ -0,0 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View file

@ -0,0 +1,49 @@
'use client'
import { useEffect, useState } from 'react'
import { Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('dark')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
const stored = localStorage.getItem('theme') as 'light' | 'dark' | null
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const initialTheme = stored || (prefersDark ? 'dark' : 'light')
setTheme(initialTheme)
}, [])
const toggleTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
if (!mounted) {
return (
<Button variant="ghost" size="icon" className="size-9 text-muted-foreground">
<span className="sr-only">Theme umschalten</span>
</Button>
)
}
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="size-9 text-muted-foreground hover:text-foreground transition-colors"
aria-label={theme === 'dark' ? 'Zum hellen Modus wechseln' : 'Zum dunklen Modus wechseln'}
>
{theme === 'dark' ? (
<Sun className="size-5" />
) : (
<Moon className="size-5" />
)}
</Button>
)
}

View file

@ -0,0 +1,66 @@
'use client'
import * as React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDownIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn('border-b last:border-b-0', className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -0,0 +1,157 @@
'use client'
import * as React from 'react'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className,
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className,
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className,
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn('text-lg font-semibold', className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

Some files were not shown because too many files have changed in this diff Show more