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)}
>
-
@@ -400,11 +435,16 @@ export default function GalleryPage() {
onClick={(e) => e.stopPropagation()}
>
-

+
+
+
+ )}
);
}
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 && (
+
+ )}
{/* 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;