127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { motion } from 'framer-motion';
|
|
import { ExternalLink, Calendar, Tag } from 'lucide-react';
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
|
|
interface Project {
|
|
slug: string;
|
|
title: string;
|
|
description: string;
|
|
excerpt?: string;
|
|
technologies: string[];
|
|
category?: string;
|
|
date?: string;
|
|
featured?: boolean;
|
|
}
|
|
|
|
interface PortfolioCardProps {
|
|
project: Project;
|
|
}
|
|
|
|
const PortfolioCard = ({ project }: PortfolioCardProps) => {
|
|
const locale = useLocale();
|
|
const t = useTranslations('portfolio.ui');
|
|
const displayTags = project.technologies;
|
|
|
|
return (
|
|
<Link
|
|
href={`/${locale}/portfolio/${project.slug}`}
|
|
className="group relative block bg-background/50 backdrop-blur-sm
|
|
rounded-xl shadow-lg hover:shadow-2xl
|
|
ring-1 ring-border hover:ring-accent
|
|
transition-all duration-500 focus:outline-none focus:ring-2
|
|
focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
|
|
transform hover:-translate-y-1"
|
|
>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5 }}
|
|
className="relative overflow-hidden rounded-xl"
|
|
>
|
|
{/* Image Container */}
|
|
<div className="aspect-[16/9] w-full relative overflow-hidden bg-background">
|
|
<Image
|
|
src={`/images/projects/${project.slug}/cover.avif`}
|
|
alt={project.title}
|
|
fill
|
|
sizes="(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px"
|
|
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
|
/>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="relative -mt-2 p-6 bg-background z-10">
|
|
{/* Project Info */}
|
|
<div className="flex items-center gap-4 mb-4 text-muted-foreground">
|
|
{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>{displayTags.length} {t('technologies')}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Title & Description */}
|
|
<h3 className="text-xl font-semibold text-foreground mb-3
|
|
group-hover:text-foreground/90 transition-colors duration-300">
|
|
{project.title}
|
|
</h3>
|
|
<p className="text-muted-foreground text-sm line-clamp-2 mb-4
|
|
group-hover:text-foreground/70 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-card/50
|
|
text-sm text-muted-foreground rounded-full
|
|
ring-1 ring-border/50 group-hover:ring-accent/50
|
|
group-hover:bg-card/70 group-hover:text-foreground/80
|
|
transition-all duration-300"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
{displayTags.length > 3 && (
|
|
<span className="text-sm text-muted-foreground/60">
|
|
+{displayTags.length - 3} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Link Icon */}
|
|
<div className="absolute top-6 right-6 p-2.5 rounded-full
|
|
bg-card/60 backdrop-blur-sm ring-1 ring-border/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-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Subtle Hover Border */}
|
|
<div className="absolute inset-0 rounded-xl pointer-events-none
|
|
ring-1 ring-ring/0 group-hover:ring-ring/30
|
|
transition-all duration-500" />
|
|
</motion.div>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
export default React.memo(PortfolioCard);
|