172 lines
6.4 KiB
TypeScript
172 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { motion } from "framer-motion";
|
|
import Image from "next/image";
|
|
import {
|
|
ShoppingCart,
|
|
Calendar,
|
|
Server,
|
|
Smartphone,
|
|
BarChart3,
|
|
Building,
|
|
} from "lucide-react";
|
|
import GlassCard from "@/components/ui/GlassCard";
|
|
import ProjectModal from "@/components/ui/ProjectModal";
|
|
import { LiveDot, InProgressBadge, ProjectLinks } from "@/components/sections/ProjectsCarousel";
|
|
import { fadeUp, stagger, viewportConfig } from "@/lib/animations";
|
|
import type { Dictionary, Locale } from "@/lib/dictionaries";
|
|
|
|
const projectIcons: Record<string, typeof ShoppingCart> = {
|
|
"client-ecommerce": ShoppingCart,
|
|
"booking-platform": Calendar,
|
|
"infra-dashboard": Server,
|
|
"mobile-companion": Smartphone,
|
|
"analytics-service": BarChart3,
|
|
"business-landing": Building,
|
|
"etf-oglasi": Smartphone,
|
|
};
|
|
|
|
const projectColors: Record<string, string> = {
|
|
"client-ecommerce": "from-blue-500/20 to-blue-600/5 text-blue-300",
|
|
"booking-platform": "from-emerald-500/20 to-emerald-600/5 text-emerald-300",
|
|
"infra-dashboard": "from-purple-500/20 to-purple-600/5 text-purple-300",
|
|
"mobile-companion": "from-rose-500/20 to-rose-600/5 text-rose-300",
|
|
"analytics-service": "from-amber-500/20 to-amber-600/5 text-amber-300",
|
|
"business-landing": "from-slate-400/20 to-slate-500/5 text-slate-300",
|
|
"etf-oglasi": "from-violet-500/20 to-violet-600/5 text-violet-300",
|
|
};
|
|
|
|
interface ProjectsShowcaseProps {
|
|
locale: Locale;
|
|
dict: Dictionary;
|
|
filter?: "client" | "personal";
|
|
sectionTitle?: string;
|
|
}
|
|
|
|
export default function ProjectsShowcase({
|
|
dict,
|
|
filter,
|
|
sectionTitle,
|
|
}: ProjectsShowcaseProps) {
|
|
let projects = [...dict.projects.items];
|
|
if (filter) projects = projects.filter((p) => p.type === filter);
|
|
projects.sort((a, b) => a.order - b.order);
|
|
|
|
type Project = (typeof projects)[number];
|
|
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
|
|
|
if (projects.length === 0) return null;
|
|
|
|
return (
|
|
<section className="relative w-full py-16 sm:py-20">
|
|
<div className="relative mx-auto w-full max-w-7xl px-6 lg:px-8">
|
|
{sectionTitle && (
|
|
<motion.h2
|
|
initial={{ opacity: 0, y: 16 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={viewportConfig}
|
|
className="mb-10 font-display text-2xl font-medium tracking-tight text-gradient-fade sm:text-3xl"
|
|
>
|
|
{sectionTitle}
|
|
</motion.h2>
|
|
)}
|
|
|
|
<motion.div
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={viewportConfig}
|
|
variants={stagger(0.08)}
|
|
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
>
|
|
{projects.map((project) => {
|
|
const Icon = projectIcons[project.slug] || Building;
|
|
const colorClass =
|
|
projectColors[project.slug] ||
|
|
"from-purple-500/20 to-purple-600/5 text-purple-300";
|
|
const gradientClasses = colorClass.split(" ").slice(0, 2).join(" ");
|
|
const iconColor = colorClass.split(" ").slice(2).join(" ");
|
|
const previewImage = project.screenshots[0];
|
|
|
|
return (
|
|
<motion.div
|
|
key={project.slug}
|
|
variants={fadeUp}
|
|
className="group h-full cursor-pointer"
|
|
onClick={() => setSelectedProject(project)}
|
|
>
|
|
<GlassCard className="flex h-full flex-col overflow-hidden p-0">
|
|
{previewImage ? (
|
|
<div className="relative h-44 w-full overflow-hidden">
|
|
<Image
|
|
src={previewImage}
|
|
alt={project.title}
|
|
fill
|
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={`flex h-36 items-center justify-center bg-gradient-to-br ${gradientClasses}`}
|
|
>
|
|
<Icon
|
|
className={`h-10 w-10 ${iconColor} transition-transform duration-500 group-hover:scale-110`}
|
|
strokeWidth={1.2}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-1 flex-col p-5">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<span
|
|
className={`rounded-md px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider ${
|
|
project.type === "client"
|
|
? "border border-purple-400/30 bg-purple-500/15 text-purple-300"
|
|
: "border border-border/50 bg-white/5 text-text-secondary"
|
|
}`}
|
|
>
|
|
{project.type === "client"
|
|
? dict.projects.clientWork
|
|
: dict.projects.personal}
|
|
</span>
|
|
{project.live && <LiveDot />}
|
|
{project.inProgress && <InProgressBadge label={dict.projects.inProgress} />}
|
|
</div>
|
|
|
|
<h3 className="mb-2 text-base font-medium text-text-primary">
|
|
{project.title}
|
|
</h3>
|
|
<p className="mb-4 text-sm leading-relaxed text-text-secondary">
|
|
{project.description}
|
|
</p>
|
|
<div className="mb-4 flex flex-wrap gap-1.5">
|
|
{project.tags.map((tag) => (
|
|
<span
|
|
key={tag}
|
|
className="rounded-md bg-surface/60 px-2 py-0.5 font-mono text-[11px] text-text-secondary"
|
|
>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div onClick={(e) => e.stopPropagation()} className="mt-auto">
|
|
<ProjectLinks project={project} dict={dict} />
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</motion.div>
|
|
</div>
|
|
|
|
<ProjectModal
|
|
project={selectedProject}
|
|
dict={dict}
|
|
onClose={() => setSelectedProject(null)}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|