coders-club / src /components /bento-gallery.tsx
kumar-aditya's picture
Upload 108 files
a7b8df9 verified
import React, { useRef, useState, useEffect } from "react"
import {
motion,
useScroll,
useTransform,
AnimatePresence,
} from "framer-motion"
import { cn } from "@/lib/utils" // Assumes a 'lib/utils.ts' file for 'cn'
import { X } from "lucide-react"
// Defines the structure for each image item in the gallery
type ImageItem = {
id: number | string
title: string
desc: string
url: string
span: string // Tailwind CSS grid span classes (e.g., "md:col-span-2")
}
// Defines the props for the main gallery component
interface InteractiveImageBentoGalleryProps {
imageItems: ImageItem[]
title: string
description: string
}
// Animation variants for the container to stagger children
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
},
},
}
// Animation variants for each gallery item
const itemVariants = {
hidden: { opacity: 0, y: 20, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring" as const, stiffness: 100, damping: 15 },
},
}
// Modal component for displaying the selected image
const ImageModal = ({
item,
onClose,
}: {
item: ImageItem
onClose: () => void
}) => {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-md p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="relative w-full max-w-4xl"
onClick={(e) => e.stopPropagation()}
>
<div className="glass-card p-4 rounded-xl">
<img
src={item.url}
alt={item.title}
className="h-auto max-h-[80vh] w-full rounded-lg object-contain"
/>
<div className="mt-4 text-center">
<h3 className="responsive-text-xl font-bold dot-background-heading">
{item.title}
</h3>
<p className="mt-2 responsive-text-base dot-background-text">
{item.desc}
</p>
</div>
</div>
</motion.div>
<button
onClick={onClose}
className="absolute right-4 top-4 p-2 rounded-full bg-black/50 backdrop-blur-sm text-white/90 transition-all hover:text-white hover:bg-black/70"
aria-label="Close image view"
>
<X size={20} />
</button>
</motion.div>
)
}
// Main gallery component
const InteractiveImageBentoGallery: React.FC<
InteractiveImageBentoGalleryProps
> = ({ imageItems, title, description }) => {
const [selectedItem, setSelectedItem] = useState<ImageItem | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const gridRef = useRef<HTMLDivElement>(null)
const targetRef = useRef<HTMLDivElement>(null)
// Framer Motion scroll animations
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start end", "end start"],
})
const opacity = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0])
const y = useTransform(scrollYProgress, [0, 0.2], [30, 0])
return (
<section
ref={targetRef}
className="relative w-full overflow-hidden py-12 sm:py-16 lg:py-24"
>
<motion.div
style={{ opacity, y }}
className="container mx-auto px-4 text-center"
>
<h2 className="responsive-text-3xl sm:responsive-text-4xl font-bold tracking-tight dot-background-heading">
{title}
</h2>
<p className="mx-auto mt-4 max-w-2xl responsive-text-lg dot-background-text">
{description}
</p>
</motion.div>
<div
ref={containerRef}
className="relative mt-8 sm:mt-12 w-full px-4 sm:px-6 lg:px-8"
>
<motion.div
ref={gridRef}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 auto-rows-[180px] sm:auto-rows-[200px] lg:auto-rows-[220px]"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
>
{imageItems.map((item) => (
<motion.div
key={item.id}
variants={itemVariants}
className={cn(
"group relative flex cursor-pointer items-end overflow-hidden rounded-lg sm:rounded-xl glass-card p-3 sm:p-4 transition-all duration-300 ease-in-out hover:scale-102 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",
item.span,
// Responsive row spans for bento layout variety
item.id === 1 || item.id === 5 || item.id === 10
? "row-span-2 sm:row-span-2 lg:row-span-2"
: "row-span-1",
// Responsive column spans
item.id === 1 ? "sm:col-span-2 lg:col-span-2" : "",
item.id === 5 ? "lg:col-span-2" : "",
)}
whileHover={{ scale: 1.02, y: -4 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
onClick={() => setSelectedItem(item)}
onKeyDown={(e) => e.key === "Enter" && setSelectedItem(item)}
tabIndex={0}
aria-label={`View ${item.title}`}
>
<img
src={item.url}
alt={item.title}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
{/* Enhanced gradient overlay for better text visibility */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/90 via-black/50 to-transparent opacity-60 group-hover:opacity-90 transition-opacity duration-500" />
{/* Always visible overlay on mobile for better accessibility */}
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 to-transparent sm:opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10 translate-y-2 sm:translate-y-4 opacity-100 sm:opacity-0 transition-all duration-500 group-hover:translate-y-0 group-hover:opacity-100">
<h3 className="responsive-text-base sm:responsive-text-lg font-bold text-white drop-shadow-lg">
{item.title}
</h3>
<p className="mt-1 responsive-text-sm text-white/90 drop-shadow-md line-clamp-2">
{item.desc}
</p>
</div>
{/* Hover indicator for desktop */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300 hidden sm:block">
<div className="w-8 h-8 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</motion.div>
))}
</motion.div>
</div>
<AnimatePresence>
{selectedItem && (
<ImageModal item={selectedItem} onClose={() => setSelectedItem(null)} />
)}
</AnimatePresence>
</section>
)
}
export default InteractiveImageBentoGallery