diff --git a/app/(pages)/about/page.tsx b/app/(pages)/about/page.tsx index 33a2630..84a8132 100644 --- a/app/(pages)/about/page.tsx +++ b/app/(pages)/about/page.tsx @@ -1,7 +1,7 @@ "use client"; import { motion } from "framer-motion"; -import { Diamond, Users, Trophy, Palette, Target, Heart } from "lucide-react"; +import { Diamond, Users, Trophy, Palette, Heart } from "lucide-react"; import Link from "next/link"; const stats = [ diff --git a/app/(pages)/contact/page.tsx b/app/(pages)/contact/page.tsx index 3829287..e295cd1 100644 --- a/app/(pages)/contact/page.tsx +++ b/app/(pages)/contact/page.tsx @@ -14,6 +14,8 @@ import { CheckCircle } from "lucide-react"; +import Link from "next/link"; + const contactMethods = [ { icon: Mail, @@ -108,12 +110,12 @@ export default function ContactPage() {

Thank you for reaching out. We'll get back to you within 24-48 hours.

- Back to Home - + @@ -314,6 +316,7 @@ export default function ContactPage() { onChange={handleChange} required rows={5} + maxLength={1000} placeholder="Describe your question or issue in detail..." className="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-white placeholder-zinc-500 focus:outline-none focus:border-lime-400 transition-colors resize-none" /> diff --git a/app/(pages)/faq/page.tsx b/app/(pages)/faq/page.tsx index 5904fff..12e6813 100644 --- a/app/(pages)/faq/page.tsx +++ b/app/(pages)/faq/page.tsx @@ -2,10 +2,10 @@ import { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; +import Link from "next/link"; import { ChevronDown, HelpCircle, - Users, Vote, Upload, Award, @@ -282,12 +282,12 @@ export default function FAQPage() {

Can't find what you're looking for? We're here to help!

- Contact Us - + diff --git a/app/(pages)/gallery/page.tsx b/app/(pages)/gallery/page.tsx index dbb6aee..a1cc89d 100644 --- a/app/(pages)/gallery/page.tsx +++ b/app/(pages)/gallery/page.tsx @@ -1,10 +1,11 @@ "use client"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; +import Image from "next/image"; import { Search, - Filter, + Filter, Heart, Monitor, Palette, @@ -133,9 +134,38 @@ export default function GalleryPage() { const [searchQuery, setSearchQuery] = useState(""); const [viewMode, setViewMode] = useState<"grid" | "large">("grid"); const [selectedArtwork, setSelectedArtwork] = useState(null); - const [votedArtworks, setVotedArtworks] = useState>(new Set()); + const [votedArtworks, setVotedArtworks] = useState>(() => { + if (typeof window !== "undefined") { + try { + const stored = localStorage.getItem("artsplash-votes"); + return stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + return new Set(); + } + } + return new Set(); + }); const [showFilters, setShowFilters] = useState(false); + // Lock body scroll when modal is open + useEffect(() => { + if (selectedArtwork) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { document.body.style.overflow = ""; }; + }, [selectedArtwork]); + + // Close modal on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setSelectedArtwork(null); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + // Use a daily seed for consistent randomization throughout the day const dailySeed = useMemo(() => { const today = new Date(); @@ -165,14 +195,16 @@ export default function GalleryPage() { 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 - }; + const handleVote = useCallback((artworkId: string) => { + if (votedArtworks.has(artworkId)) return; + setVotedArtworks(prev => { + const next = new Set([...prev, artworkId]); + try { + localStorage.setItem("artsplash-votes", JSON.stringify([...next])); + } catch { /* storage full - ignore */ } + return next; + }); + }, [votedArtworks]); return (
@@ -328,10 +360,13 @@ export default function GalleryPage() { className="relative aspect-square cursor-pointer overflow-hidden" onClick={() => setSelectedArtwork(artwork)} > - {artwork.title}
@@ -400,11 +435,16 @@ export default function GalleryPage() { onClick={(e) => e.stopPropagation()} >
- {selectedArtwork.title} +
+ {selectedArtwork.title} +
+ )}
); } diff --git a/app/(pages)/submit/page.tsx b/app/(pages)/submit/page.tsx index d875eaa..e983ca9 100644 --- a/app/(pages)/submit/page.tsx +++ b/app/(pages)/submit/page.tsx @@ -4,7 +4,6 @@ import { useState, useRef } from "react"; import { motion } from "framer-motion"; import { Upload, - Image as ImageIcon, FileText, X, CheckCircle, @@ -46,6 +45,7 @@ export default function SubmitPage() { }); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); + const [fileError, setFileError] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [submitSuccess, setSubmitSuccess] = useState(false); @@ -55,13 +55,15 @@ export default function SubmitPage() { const validTypes = ["image/jpeg", "image/png", "application/pdf"]; const maxSize = 10 * 1024 * 1024; // 10MB + setFileError(null); + if (!validTypes.includes(selectedFile.type)) { - alert("Please upload a JPG, PNG, or PDF file."); + setFileError("Invalid file type. Please upload a JPG, PNG, or PDF file."); return; } if (selectedFile.size > maxSize) { - alert("File size must be less than 10MB."); + setFileError("File too large. Maximum size is 10MB."); return; } @@ -97,6 +99,7 @@ export default function SubmitPage() { const removeFile = () => { setFile(null); setPreview(null); + setFileError(null); if (fileInputRef.current) fileInputRef.current.value = ""; }; @@ -125,6 +128,7 @@ export default function SubmitPage() { setFormData({ title: "", description: "", category: "" }); setFile(null); setPreview(null); + setFileError(null); }} className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-8 py-4 rounded-full font-bold hover:bg-lime-300 transition-all" > @@ -300,6 +304,12 @@ export default function SubmitPage() { )} + {fileError && ( +
+ + {fileError} +
+ )} {/* Title & Description */} diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index e4232d9..ea90a8d 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -8,23 +8,29 @@ function useGridSize() { const [gridSize, setGridSize] = useState({ cols: 20, rows: 12 }); useEffect(() => { + let timeout: ReturnType; const updateGridSize = () => { const width = window.innerWidth; if (width < 640) { - // Mobile setGridSize({ cols: 8, rows: 10 }); } else if (width < 1024) { - // Tablet setGridSize({ cols: 12, rows: 10 }); } else { - // Desktop setGridSize({ cols: 20, rows: 12 }); } }; + const debouncedUpdate = () => { + clearTimeout(timeout); + timeout = setTimeout(updateGridSize, 150); + }; + updateGridSize(); - window.addEventListener("resize", updateGridSize); - return () => window.removeEventListener("resize", updateGridSize); + window.addEventListener("resize", debouncedUpdate); + return () => { + clearTimeout(timeout); + window.removeEventListener("resize", debouncedUpdate); + }; }, []); return gridSize; @@ -68,6 +74,7 @@ function TileGrid() { return (
{/* Front - Black */} diff --git a/app/globals.css b/app/globals.css index 58b9faa..69f371a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -45,6 +45,11 @@ body { animation-play-state: paused; } +.animate-marquee-slow { + animation: marquee 30s linear infinite; + width: max-content; +} + /* Smooth scrolling */ html { scroll-behavior: smooth;