Initial import: was v0 exportiert hat
This commit is contained in:
commit
0194931215
167 changed files with 16465 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
|
||||
37
Jamulix_40_Jahre_Code.html
Normal file
37
Jamulix_40_Jahre_Code.html
Normal file
File diff suppressed because one or more lines are too long
73
app/datenschutz/page.tsx
Normal file
73
app/datenschutz/page.tsx
Normal 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
114
app/globals.css
Normal 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
61
app/impressum/page.tsx
Normal 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
90
app/layout.tsx
Normal 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
23
app/page.tsx
Normal 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
21
components.json
Normal 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"
|
||||
}
|
||||
120
components/about-section.tsx
Normal file
120
components/about-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
122
components/focus-section.tsx
Normal file
122
components/focus-section.tsx
Normal 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
128
components/footer.tsx
Normal 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">
|
||||
© {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
98
components/header.tsx
Normal 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
108
components/hero.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
88
components/philosophy-section.tsx
Normal file
88
components/philosophy-section.tsx
Normal 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">
|
||||
“Der beste Code ist der, den man in zehn Jahren noch versteht –
|
||||
geschrieben von jemandem, der in zehn Jahren noch lernt.”
|
||||
</blockquote>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
154
components/projects-section.tsx
Normal file
154
components/projects-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
119
components/system-diagram.tsx
Normal file
119
components/system-diagram.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
components/theme-provider.tsx
Normal file
11
components/theme-provider.tsx
Normal 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>
|
||||
}
|
||||
49
components/theme-toggle.tsx
Normal file
49
components/theme-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
66
components/ui/accordion.tsx
Normal file
66
components/ui/accordion.tsx
Normal 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 }
|
||||
157
components/ui/alert-dialog.tsx
Normal file
157
components/ui/alert-dialog.tsx
Normal 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
Loading…
Reference in a new issue