Migrate from Vite to Next.js 15 with SSR

- Replace Vite + React Router with Next.js 15 App Router
- Implement i18n with next-intl (URL-based: /de, /en, /sr)
- Add SSR/SSG for all pages (48 static pages generated)
- Setup Supabase SSR client for auth
- Migrate all pages: Home, About, Portfolio, Blog, Contact, Login, Dashboard, Imprint, Privacy, Terms
- Add Docker support with standalone output
- Replace i18next with next-intl JSON translations
- Use next/image for optimized images

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 01:00:33 +01:00
co-authored by Claude Opus 4.5
parent a66f51b9a2
commit b1ec7b4d61
281 changed files with 8024 additions and 8901 deletions
@@ -0,0 +1,90 @@
"use client"
import { useTranslation } from 'react-i18next';
import { motion } from "framer-motion";
import { Code2, ShoppingBag, TrendingUp } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../../components/Card";
import { Badge } from "../../../components/Badge";
// Definiere den Typ für einen Expertise-Bereich
interface ExpertiseArea {
title: string;
description: string;
skills: string[];
}
// Typisierung für iconMap: Wir erwarten ein React-Komponenten-Typ, der optional eine className erhält
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
'ERP Integration': ShoppingBag,
'Entwicklung': Code2,
'E-Commerce': ShoppingBag,
'Digitales Marketing': TrendingUp,
// English mappings
'Development': Code2,
'Digital Marketing': TrendingUp
};
const Expertise = () => {
const { t } = useTranslation('home');
// Wir casten den Rückgabewert explizit zu ExpertiseArea[]
const expertiseAreas = t('expertise.areas', { returnObjects: true }) as ExpertiseArea[];
return (
<section className="py-12 sm:py-24 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.h2
className="text-3xl sm:text-4xl font-bold text-foreground text-center mb-12"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{t('expertise.title')}
</motion.h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8">
{expertiseAreas.map((area: ExpertiseArea, index: number) => {
const Icon = iconMap[area.title] || ShoppingBag;
return (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="h-full bg-card hover:bg-accent transition-all duration-300 ease-in-out transform hover:-translate-y-2">
<CardHeader>
<div className="w-12 h-12 bg-primary rounded-full flex items-center justify-center mb-4">
<Icon className="w-6 h-6 text-primary-foreground" />
</div>
<CardTitle className="text-xl font-semibold text-foreground">
{area.title}
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-muted-foreground mb-4">
{area.description}
</CardDescription>
<div className="flex flex-wrap gap-2">
{area.skills.map((skill: string, skillIndex: number) => (
<Badge
key={skillIndex}
variant="secondary"
className="bg-secondary text-secondary-foreground hover:bg-primary hover:text-primary-foreground transition-colors duration-200"
>
{skill}
</Badge>
))}
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default Expertise;