jamulix-homepage/components/projects-section.tsx

154 lines
5 KiB
TypeScript

'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 Daily AI Reports',
slug: 'hackernews',
description: 'KI Tools werten zweimal täglich die Hacker News aus und beurteilen deren Relevanz in Bezug auf KI.',
tech: ['Python', 'FastAPI', 'Openrouter', 'deepseek-reasoner', 'qwen3-max'],
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 junge 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>
)
}