Adjusted the Marquee Speed
This commit is contained in:
463
app/(pages)/gallery/page.tsx
Normal file
463
app/(pages)/gallery/page.tsx
Normal file
@@ -0,0 +1,463 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user