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