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:
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ExternalLink, Calendar, Tag } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { trackProjectClick } from '../../../services/gtm';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
slug: string;
|
||||
date?: string;
|
||||
excerpt?: string;
|
||||
technologies?: string[];
|
||||
category?: string;
|
||||
}
|
||||
|
||||
interface PortfolioCardProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const basePath = `/images/projects/${project.slug}`;
|
||||
const imagePath = `${basePath}/cover.jpg`;
|
||||
const srcSetJpg = `${basePath}/cover-200w.jpg 200w, ${basePath}/cover-300w.jpg 300w, ${basePath}/cover-400w.jpg 400w, ${basePath}/cover-600w.jpg 600w, ${basePath}/cover-800w.jpg 800w, ${basePath}/cover-1200w.jpg 1200w`;
|
||||
const srcSetWebP = `${basePath}/cover-200w.webp 200w, ${basePath}/cover-300w.webp 300w, ${basePath}/cover-400w.webp 400w, ${basePath}/cover-600w.webp 600w, ${basePath}/cover-800w.webp 800w, ${basePath}/cover-1200w.webp 1200w`;
|
||||
// Mobile: ~350px, Tablet: ~400px, Desktop 2-col: ~550px, Desktop 3-col: ~400px
|
||||
const imageSizes = "(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px";
|
||||
|
||||
// Verwende technologies anstelle von tags wenn verfügbar
|
||||
const displayTags = project.technologies || project.tags;
|
||||
|
||||
const handleClick = () => {
|
||||
trackProjectClick(project.title, project.category);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/portfolio/${project.slug}`}
|
||||
onClick={handleClick}
|
||||
className="group relative block bg-zinc-900/50 backdrop-blur-sm
|
||||
rounded-xl shadow-lg hover:shadow-2xl
|
||||
ring-1 ring-zinc-800 hover:ring-zinc-700
|
||||
transition-all duration-500 focus:outline-none focus:ring-2
|
||||
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950
|
||||
transform hover:-translate-y-1"
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative overflow-hidden rounded-xl"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'opacity, transform'
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900">
|
||||
<picture>
|
||||
<source
|
||||
srcSet={srcSetWebP}
|
||||
sizes={imageSizes}
|
||||
type="image/webp"
|
||||
/>
|
||||
<source
|
||||
srcSet={srcSetJpg}
|
||||
sizes={imageSizes}
|
||||
type="image/jpeg"
|
||||
/>
|
||||
<img
|
||||
src={imagePath}
|
||||
alt={project.title}
|
||||
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700
|
||||
group-hover:scale-110"
|
||||
loading="lazy"
|
||||
width={400}
|
||||
height={225}
|
||||
decoding="async"
|
||||
style={{ transformOrigin: 'center center' }}
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
{/* Content - with relative positioning to overlap the gradient */}
|
||||
<div className="relative -mt-2 p-6 bg-zinc-900 z-10">
|
||||
{/* Project Info */}
|
||||
<div className="flex items-center gap-4 mb-4 text-zinc-500">
|
||||
{project.date && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>{project.date}</span>
|
||||
</div>
|
||||
)}
|
||||
{displayTags && displayTags.length > 0 && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Tag className="h-4 w-4" />
|
||||
<span>
|
||||
{t('portfolio.ui.technologiesCount', {
|
||||
count: displayTags.length,
|
||||
defaultValue: '{{count}} Technologies'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title & Description */}
|
||||
<h3 className="text-xl font-semibold text-white mb-3
|
||||
group-hover:text-zinc-100 transition-colors duration-300">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-zinc-400 text-sm line-clamp-2 mb-4
|
||||
group-hover:text-zinc-300 transition-colors duration-300">
|
||||
{project.excerpt || project.description}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{displayTags.slice(0, 3).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 bg-zinc-800/50
|
||||
text-sm text-zinc-400 rounded-full
|
||||
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50
|
||||
group-hover:bg-zinc-800/70 group-hover:text-zinc-300
|
||||
transition-all duration-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{displayTags.length > 3 && (
|
||||
<span className="text-sm text-zinc-600">
|
||||
{t('portfolio.ui.moreTechnologies', {
|
||||
count: displayTags.length - 3,
|
||||
defaultValue: '+{{count}} more'
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Link Icon */}
|
||||
<div className="absolute top-6 right-6 p-2.5 rounded-full
|
||||
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50
|
||||
opacity-0 group-hover:opacity-100
|
||||
transform translate-y-2 group-hover:translate-y-0
|
||||
transition-all duration-300">
|
||||
<ExternalLink className="h-4 w-4 text-zinc-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle Hover Border */}
|
||||
<div className="absolute inset-0 rounded-xl pointer-events-none
|
||||
ring-1 ring-zinc-600/0 group-hover:ring-zinc-600/30
|
||||
transition-all duration-500" />
|
||||
</motion.div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default PortfolioCard;
|
||||
Reference in New Issue
Block a user