Jade-Infra-test / src /components /ProjectCard.jsx
rushiljain's picture
Upload 29 files
691cdd0 verified
import React, { useRef } from 'react';
import { Link } from 'react-router-dom';
import ImageFlex from './ImageFlex.jsx';
export default function ProjectCard({ project }) {
const cardRef = useRef(null);
function getStatusClasses(status) {
if (!status) return 'bg-slate-100 text-slate-700';
switch (status) {
case 'Completed':
return 'bg-emerald-100 text-emerald-700';
case 'Ongoing':
return 'bg-amber-100 text-amber-700';
case 'Upcoming':
return 'bg-sky-100 text-sky-700';
default:
return 'bg-slate-100 text-slate-700';
}
}
function onMove(e) {
const el = cardRef.current;
if (!el) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const rect = el.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const midX = rect.width / 2;
const midY = rect.height / 2;
const rotateX = ((y - midY) / midY) * -4;
const rotateY = ((x - midX) / midX) * 4;
el.style.transform = `perspective(800px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
}
function onLeave() {
const el = cardRef.current;
if (!el) return;
el.style.transform = 'perspective(800px) rotateX(0deg) rotateY(0deg)';
}
const hasGallery = Array.isArray(project.gallery) && project.gallery.length > 0;
const primaryImage =
(typeof project.image === 'string' && project.image.replace(/\.(jpg|jpeg|png|webp)$/i, '')) || null;
const galleryImage = hasGallery ? project.gallery[0] : null;
const explicitBase =
typeof project.imageBase === 'string'
? project.imageBase.replace(/\.(jpg|jpeg|png|webp)$/i, '')
: null;
const slugFallback = !project.noDetail && project.slug ? `/assets/projects/${project.slug}/gallery-1` : null;
const imageBases = [
primaryImage,
galleryImage,
explicitBase,
slugFallback
].filter(Boolean);
const hasImage = imageBases.length > 0;
return (
<article
ref={cardRef}
onMouseMove={onMove}
onMouseLeave={onLeave}
className={`relative card group overflow-hidden will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:bg-brand-600/0 before:blur before:transition before:duration-300 hover:before:bg-brand-600/10 ${
hasImage ? 'aspect-[3/4] min-h-[400px] md:min-h-[500px]' : 'h-full'
}`}
aria-labelledby={`proj-${project.slug}`}
>
{hasImage ? (
<>
<div className="relative h-full w-full bg-slate-200">
<ImageFlex
base={imageBases[0]}
srcCandidates={imageBases.flatMap((base) =>
['webp', 'jpg', 'jpeg', 'png'].map((ext) => `${base}.${ext}`)
)}
alt={project.title}
className="h-full w-full object-contain transition-transform group-hover:scale-[1.03]"
loading="lazy"
/>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-900/80 via-slate-900/40 to-transparent"></div>
</div>
{/* Overlay text content */}
<div className="absolute bottom-0 left-0 right-0 p-5 text-white">
<h3 id={`proj-${project.slug}`} className="h3 text-white">
{project.noDetail ? (
<span>{project.title}</span>
) : (
<Link to={`/project/${project.slug}`} className="hover:underline">
{project.title}
</Link>
)}
</h3>
<p className="mt-1 text-sm text-white/90">{project.location}</p>
{project.projectSize && (
<p className="mt-1 text-sm font-medium text-white">
{project.projectSize}
</p>
)}
<div className="mt-4 flex items-center justify-between">
<span className="flex flex-wrap items-center gap-2">
{project.status && (
<span
className={`rounded px-2 py-1 text-xs font-medium ${getStatusClasses(project.status)}`}
aria-label={`Status: ${project.status}`}
>
{project.status}
</span>
)}
{(project.categories || []).slice(0, 2).map((c) => (
<span key={c} className="rounded bg-white/20 backdrop-blur-sm px-2 py-1 text-xs text-white">
{c}
</span>
))}
</span>
{!project.noDetail && (
<Link
to={`/project/${project.slug}`}
className="text-sm font-medium text-white underline-offset-2 hover:underline"
aria-label={`View ${project.title}`}
>
View details
</Link>
)}
</div>
</div>
</>
) : (
<>
<div className="flex h-24 w-full items-center justify-center bg-gradient-to-br from-brand-50 via-white to-brand-100">
<svg
className="h-12 w-12 text-brand-600"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
</div>
<div className="p-5">
<h3 id={`proj-${project.slug}`} className="h3">
{project.noDetail ? (
<span>{project.title}</span>
) : (
<Link to={`/project/${project.slug}`} className="hover:underline">
{project.title}
</Link>
)}
</h3>
<p className="mt-1 text-sm text-slate-600">{project.location}</p>
{project.projectSize && (
<p className="mt-1 text-sm font-medium text-brand-700">
{project.projectSize}
</p>
)}
<div className="mt-4 flex items-center justify-between">
<span className="flex flex-wrap items-center gap-2">
{project.status && (
<span
className={`rounded px-2 py-1 text-xs font-medium ${getStatusClasses(project.status)}`}
aria-label={`Status: ${project.status}`}
>
{project.status}
</span>
)}
{(project.categories || []).slice(0, 2).map((c) => (
<span key={c} className="rounded bg-slate-100 px-2 py-1 text-xs text-slate-700">
{c}
</span>
))}
</span>
{!project.noDetail && (
<Link
to={`/project/${project.slug}`}
className="text-sm font-medium text-brand-700 underline-offset-2 hover:underline"
aria-label={`View ${project.title}`}
>
View details
</Link>
)}
</div>
</div>
</>
)}
</article>
);
}