Files
ArtSplash/app/(pages)/gallery/page.tsx

464 lines
18 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Search,
Filter,
Heart,
Monitor,
Palette,
FileImage,
Grid,
LayoutGrid,
X,
ChevronDown,
Eye
} from "lucide-react";
// Mock data - In production, this would come from the API
const mockArtworks = [
{
id: "1",
title: "Digital Dreams",
artist: "Rahul K.",
category: "digital-art",
imageUrl: "https://images.unsplash.com/photo-1549490349-8643362247b5?w=600&h=600&fit=crop",
department: "CSE",
year: "3rd Year",
},
{
id: "2",
title: "Ocean Serenity",
artist: "Priya M.",
category: "paintings",
imageUrl: "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=600&h=600&fit=crop",
department: "ECE",
year: "2nd Year",
},
{
id: "3",
title: "Climate Action Now",
artist: "Arun S.",
category: "poster-making",
imageUrl: "https://images.unsplash.com/photo-1561214115-f2f134cc4912?w=600&h=600&fit=crop",
department: "ISE",
year: "4th Year",
},
{
id: "4",
title: "Neon Nights",
artist: "Sneha R.",
category: "digital-art",
imageUrl: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=600&h=600&fit=crop",
department: "CSE",
year: "2nd Year",
},
{
id: "5",
title: "Mountain Sunrise",
artist: "Karthik V.",
category: "paintings",
imageUrl: "https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?w=600&h=600&fit=crop",
department: "ME",
year: "3rd Year",
},
{
id: "6",
title: "Tech Innovation",
artist: "Divya N.",
category: "poster-making",
imageUrl: "https://images.unsplash.com/photo-1513364776144-60967b0f800f?w=600&h=600&fit=crop",
department: "AIML",
year: "2nd Year",
},
{
id: "7",
title: "Abstract Emotions",
artist: "Vijay K.",
category: "digital-art",
imageUrl: "https://images.unsplash.com/photo-1547826039-bfc35e0f1ea8?w=600&h=600&fit=crop",
department: "CSE",
year: "4th Year",
},
{
id: "8",
title: "Sunset Reflections",
artist: "Meera S.",
category: "paintings",
imageUrl: "https://images.unsplash.com/photo-1578926288207-a90a5366759d?w=600&h=600&fit=crop",
department: "EEE",
year: "3rd Year",
},
{
id: "9",
title: "Save Our Planet",
artist: "Nikhil B.",
category: "poster-making",
imageUrl: "https://images.unsplash.com/photo-1482160549825-59d1b23cb208?w=600&h=600&fit=crop",
department: "CE",
year: "2nd Year",
},
];
const categories = [
{ id: "all", label: "All Categories", icon: LayoutGrid },
{ id: "digital-art", label: "Digital Art", icon: Monitor },
{ id: "paintings", label: "Paintings", icon: Palette },
{ id: "poster-making", label: "Poster Making", icon: FileImage },
];
// Seeded random shuffle function
function seededShuffle<T>(array: T[], seed: number): T[] {
const shuffled = [...array];
let currentIndex = shuffled.length;
// Simple seeded random number generator
const seededRandom = () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
while (currentIndex > 0) {
const randomIndex = Math.floor(seededRandom() * currentIndex);
currentIndex--;
[shuffled[currentIndex], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[currentIndex]];
}
return shuffled;
}
export default function GalleryPage() {
const [selectedCategory, setSelectedCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "large">("grid");
const [selectedArtwork, setSelectedArtwork] = useState<typeof mockArtworks[0] | null>(null);
const [votedArtworks, setVotedArtworks] = useState<Set<string>>(new Set());
const [showFilters, setShowFilters] = useState(false);
// Use a daily seed for consistent randomization throughout the day
const dailySeed = useMemo(() => {
const today = new Date();
return today.getFullYear() * 10000 + (today.getMonth() + 1) * 100 + today.getDate();
}, []);
// Filter and shuffle artworks
const filteredArtworks = useMemo(() => {
let result = mockArtworks;
// Filter by category
if (selectedCategory !== "all") {
result = result.filter(art => art.category === selectedCategory);
}
// Filter by search query
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter(
art =>
art.title.toLowerCase().includes(query) ||
art.artist.toLowerCase().includes(query)
);
}
// Shuffle with seed
return seededShuffle(result, dailySeed);
}, [selectedCategory, searchQuery, dailySeed]);
const handleVote = (artworkId: string) => {
if (votedArtworks.has(artworkId)) {
// Already voted - in production, show a message
return;
}
setVotedArtworks(prev => new Set([...prev, artworkId]));
// TODO: Send vote to API
};
return (
<main className="min-h-screen bg-black pt-32 pb-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
GALLERY
</span>
<h1 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Explore <span className="text-lime-400">Artworks</span>
</h1>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Browse through amazing artworks submitted by talented artists from SJEC.
Vote for your favorites to help them win!
</p>
</motion.div>
{/* Filters Bar */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="mb-8"
>
<div className="flex flex-col lg:flex-row gap-4 items-stretch lg:items-center justify-between">
{/* Search */}
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by title or artist..."
className="w-full bg-zinc-900 border border-zinc-800 rounded-xl pl-12 pr-4 py-3 text-white placeholder-zinc-500 focus:outline-none focus:border-lime-400 transition-colors"
/>
</div>
{/* Category Pills - Desktop */}
<div className="hidden lg:flex items-center gap-2">
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === cat.id
? "bg-lime-400 text-black"
: "bg-zinc-900 text-zinc-400 hover:text-white border border-zinc-800"
}`}
>
<cat.icon className="w-4 h-4" />
{cat.label}
</button>
))}
</div>
{/* Mobile Filter Button */}
<button
onClick={() => setShowFilters(!showFilters)}
className="lg:hidden flex items-center justify-center gap-2 bg-zinc-900 border border-zinc-800 text-white px-4 py-3 rounded-xl"
>
<Filter className="w-5 h-5" />
Filters
<ChevronDown className={`w-4 h-4 transition-transform ${showFilters ? "rotate-180" : ""}`} />
</button>
{/* View Toggle */}
<div className="hidden sm:flex items-center gap-2 bg-zinc-900 border border-zinc-800 rounded-xl p-1">
<button
onClick={() => setViewMode("grid")}
className={`p-2 rounded-lg transition-colors ${
viewMode === "grid" ? "bg-lime-400 text-black" : "text-zinc-400 hover:text-white"
}`}
>
<Grid className="w-5 h-5" />
</button>
<button
onClick={() => setViewMode("large")}
className={`p-2 rounded-lg transition-colors ${
viewMode === "large" ? "bg-lime-400 text-black" : "text-zinc-400 hover:text-white"
}`}
>
<LayoutGrid className="w-5 h-5" />
</button>
</div>
</div>
{/* Mobile Category Filters */}
<AnimatePresence>
{showFilters && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="lg:hidden overflow-hidden"
>
<div className="flex flex-wrap gap-2 pt-4">
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => {
setSelectedCategory(cat.id);
setShowFilters(false);
}}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === cat.id
? "bg-lime-400 text-black"
: "bg-zinc-900 text-zinc-400 border border-zinc-800"
}`}
>
<cat.icon className="w-4 h-4" />
{cat.label}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
{/* Results Count */}
<div className="text-zinc-500 text-sm mb-6">
Showing {filteredArtworks.length} artwork{filteredArtworks.length !== 1 ? "s" : ""}
</div>
{/* Gallery Grid */}
{filteredArtworks.length > 0 ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
className={`grid gap-6 ${
viewMode === "grid"
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
: "grid-cols-1 sm:grid-cols-2"
}`}
>
{filteredArtworks.map((artwork, index) => (
<motion.div
key={artwork.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
className="group bg-zinc-900 border border-zinc-800 rounded-2xl overflow-hidden hover:border-zinc-700 transition-all"
>
{/* Image */}
<div
className="relative aspect-square cursor-pointer overflow-hidden"
onClick={() => setSelectedArtwork(artwork)}
>
<img
src={artwork.imageUrl}
alt={artwork.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
<Eye className="w-8 h-8 text-white" />
</div>
{/* Category Badge */}
<div className="absolute top-3 left-3">
<span className="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium bg-black/70 text-white backdrop-blur-sm">
{artwork.category === "digital-art" && <Monitor className="w-3 h-3" />}
{artwork.category === "paintings" && <Palette className="w-3 h-3" />}
{artwork.category === "poster-making" && <FileImage className="w-3 h-3" />}
{categories.find(c => c.id === artwork.category)?.label}
</span>
</div>
</div>
{/* Info */}
<div className="p-4">
<h3 className="text-white font-bold text-lg mb-1 truncate">{artwork.title}</h3>
<p className="text-zinc-400 text-sm mb-4">
by {artwork.artist} {artwork.department}
</p>
{/* Vote Button */}
<button
onClick={() => handleVote(artwork.id)}
disabled={votedArtworks.has(artwork.id)}
className={`w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl font-semibold transition-all ${
votedArtworks.has(artwork.id)
? "bg-lime-400/20 text-lime-400 cursor-default"
: "bg-zinc-800 text-white hover:bg-lime-400 hover:text-black"
}`}
>
<Heart className={`w-5 h-5 ${votedArtworks.has(artwork.id) ? "fill-lime-400" : ""}`} />
{votedArtworks.has(artwork.id) ? "Voted!" : "Vote for this"}
</button>
</div>
</motion.div>
))}
</motion.div>
) : (
<div className="text-center py-20">
<div className="w-16 h-16 rounded-full bg-zinc-900 flex items-center justify-center mx-auto mb-4">
<Search className="w-8 h-8 text-zinc-600" />
</div>
<h3 className="text-white font-bold text-xl mb-2">No artworks found</h3>
<p className="text-zinc-500">Try adjusting your search or filter criteria</p>
</div>
)}
</div>
{/* Artwork Modal */}
<AnimatePresence>
{selectedArtwork && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={() => setSelectedArtwork(null)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-zinc-900 border border-zinc-800 rounded-3xl overflow-hidden max-w-4xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="relative">
<img
src={selectedArtwork.imageUrl}
alt={selectedArtwork.title}
className="w-full aspect-video object-cover"
/>
<button
onClick={() => setSelectedArtwork(null)}
className="absolute top-4 right-4 p-2 bg-black/50 backdrop-blur-sm rounded-full hover:bg-black/70 transition-colors"
>
<X className="w-6 h-6 text-white" />
</button>
</div>
<div className="p-6 md:p-8">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-6">
<div>
<h2 className="text-2xl md:text-3xl font-black text-white mb-2">
{selectedArtwork.title}
</h2>
<p className="text-zinc-400">
by <span className="text-lime-400">{selectedArtwork.artist}</span>
</p>
</div>
<span className="inline-flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium bg-zinc-800 text-white">
{selectedArtwork.category === "digital-art" && <Monitor className="w-4 h-4" />}
{selectedArtwork.category === "paintings" && <Palette className="w-4 h-4" />}
{selectedArtwork.category === "poster-making" && <FileImage className="w-4 h-4" />}
{categories.find(c => c.id === selectedArtwork.category)?.label}
</span>
</div>
<div className="grid sm:grid-cols-2 gap-4 mb-6">
<div className="bg-zinc-800/50 rounded-xl p-4">
<p className="text-zinc-500 text-sm mb-1">Department</p>
<p className="text-white font-semibold">{selectedArtwork.department}</p>
</div>
<div className="bg-zinc-800/50 rounded-xl p-4">
<p className="text-zinc-500 text-sm mb-1">Year</p>
<p className="text-white font-semibold">{selectedArtwork.year}</p>
</div>
</div>
<button
onClick={() => handleVote(selectedArtwork.id)}
disabled={votedArtworks.has(selectedArtwork.id)}
className={`w-full flex items-center justify-center gap-2 px-6 py-4 rounded-xl font-bold transition-all ${
votedArtworks.has(selectedArtwork.id)
? "bg-lime-400/20 text-lime-400 cursor-default"
: "bg-lime-400 text-black hover:bg-lime-300"
}`}
>
<Heart className={`w-5 h-5 ${votedArtworks.has(selectedArtwork.id) ? "fill-lime-400" : ""}`} />
{votedArtworks.has(selectedArtwork.id) ? "You voted for this artwork!" : "Vote for this Artwork"}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</main>
);
}