Removed unused imports
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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() {
|
||||
<p className="text-zinc-400 text-lg mb-8">
|
||||
Thank you for reaching out. We'll get back to you within 24-48 hours.
|
||||
</p>
|
||||
<a
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center bg-zinc-900 border border-zinc-800 text-white px-6 py-3 rounded-xl font-semibold hover:border-zinc-700 transition-colors"
|
||||
>
|
||||
Back to Home
|
||||
</a>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
<p className="text-zinc-400 mb-6">
|
||||
Can't find what you're looking for? We're here to help!
|
||||
</p>
|
||||
<a
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-6 py-3 rounded-xl font-bold hover:bg-lime-300 transition-colors"
|
||||
>
|
||||
Contact Us
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -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<typeof mockArtworks[0] | null>(null);
|
||||
const [votedArtworks, setVotedArtworks] = useState<Set<string>>(new Set());
|
||||
const [votedArtworks, setVotedArtworks] = useState<Set<string>>(() => {
|
||||
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 (
|
||||
<main className="min-h-screen bg-black pt-32 pb-20">
|
||||
@@ -328,10 +360,13 @@ export default function GalleryPage() {
|
||||
className="relative aspect-square cursor-pointer overflow-hidden"
|
||||
onClick={() => setSelectedArtwork(artwork)}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={artwork.imageUrl}
|
||||
alt={artwork.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
loading="lazy"
|
||||
/>
|
||||
<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" />
|
||||
@@ -400,11 +435,16 @@ export default function GalleryPage() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={selectedArtwork.imageUrl}
|
||||
alt={selectedArtwork.title}
|
||||
className="w-full aspect-video object-cover"
|
||||
/>
|
||||
<div className="relative aspect-video">
|
||||
<Image
|
||||
src={selectedArtwork.imageUrl}
|
||||
alt={selectedArtwork.title}
|
||||
fill
|
||||
sizes="(max-width: 1024px) 100vw, 896px"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<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"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Mail,
|
||||
Lock,
|
||||
User,
|
||||
Phone,
|
||||
GraduationCap,
|
||||
@@ -38,13 +37,20 @@ export default function RegisterPage() {
|
||||
agreeToRules: false,
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isRegistered, setIsRegistered] = useState(false);
|
||||
const [isEmailValid, setIsEmailValid] = useState<boolean | null>(null);
|
||||
const [isPhoneValid, setIsPhoneValid] = useState<boolean | null>(null);
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const sjecEmailRegex = /^[a-zA-Z0-9._%+-]+@sjec\.ac\.in$/;
|
||||
return sjecEmailRegex.test(email);
|
||||
};
|
||||
|
||||
const validatePhone = (phone: string) => {
|
||||
const phoneRegex = /^(\+91[\s-]?)?[6-9]\d{9}$/;
|
||||
return phoneRegex.test(phone.replace(/\s/g, ""));
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const email = e.target.value;
|
||||
setFormData({ ...formData, email });
|
||||
@@ -55,18 +61,72 @@ export default function RegisterPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const phone = e.target.value;
|
||||
setFormData({ ...formData, phone });
|
||||
if (phone.length > 0) {
|
||||
setIsPhoneValid(validatePhone(phone));
|
||||
} else {
|
||||
setIsPhoneValid(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid =
|
||||
formData.name.trim().length > 0 &&
|
||||
isEmailValid === true &&
|
||||
isPhoneValid !== false &&
|
||||
formData.phone.trim().length > 0 &&
|
||||
formData.department.length > 0 &&
|
||||
formData.year.length > 0 &&
|
||||
formData.agreeToRules;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isEmailValid) return;
|
||||
if (!isFormValid) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
// TODO: Implement actual registration logic
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setIsSubmitting(false);
|
||||
setIsRegistered(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black pt-32 pb-20">
|
||||
{isRegistered ? (
|
||||
<div className="max-w-xl mx-auto px-4 sm:px-6 lg:px-8 text-center pt-20">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="w-20 h-20 rounded-full bg-lime-400 flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="w-10 h-10 text-black" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-black text-white mb-4">
|
||||
Registration Successful!
|
||||
</h1>
|
||||
<p className="text-zinc-400 text-lg mb-8">
|
||||
Welcome to ArtSplash 2026! You can now submit your artwork.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/submit"
|
||||
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"
|
||||
>
|
||||
Submit Artwork
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-8 py-4 rounded-full font-bold border border-zinc-700 hover:border-zinc-500 transition-all"
|
||||
>
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-start">
|
||||
{/* Left Column - Info */}
|
||||
@@ -210,11 +270,31 @@ export default function RegisterPage() {
|
||||
type="tel"
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl pl-12 pr-4 py-3 text-white placeholder-zinc-500 focus:outline-none focus:border-lime-400 transition-colors"
|
||||
onChange={handlePhoneChange}
|
||||
className={`w-full bg-zinc-800 border rounded-xl pl-12 pr-12 py-3 text-white placeholder-zinc-500 focus:outline-none transition-colors ${
|
||||
isPhoneValid === null
|
||||
? "border-zinc-700 focus:border-lime-400"
|
||||
: isPhoneValid
|
||||
? "border-lime-400"
|
||||
: "border-red-500"
|
||||
}`}
|
||||
placeholder="+91 XXXXX XXXXX"
|
||||
/>
|
||||
{isPhoneValid !== null && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
||||
{isPhoneValid ? (
|
||||
<CheckCircle className="w-5 h-5 text-lime-400" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isPhoneValid === false && (
|
||||
<p className="text-red-500 text-sm mt-2">
|
||||
Please enter a valid Indian phone number
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Department & Year */}
|
||||
@@ -282,7 +362,7 @@ export default function RegisterPage() {
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isEmailValid}
|
||||
disabled={isSubmitting || !isFormValid}
|
||||
className="w-full flex items-center justify-center gap-2 bg-lime-400 text-black px-6 py-4 rounded-xl font-bold tracking-wider hover:bg-lime-300 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
@@ -309,6 +389,7 @@ export default function RegisterPage() {
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<File | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [fileError, setFileError] = useState<string | null>(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() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{fileError && (
|
||||
<div className="flex items-center gap-2 mt-3 text-red-500 text-sm">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
{fileError}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Title & Description */}
|
||||
|
||||
@@ -8,23 +8,29 @@ function useGridSize() {
|
||||
const [gridSize, setGridSize] = useState({ cols: 20, rows: 12 });
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
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 (
|
||||
<div
|
||||
key={`grid-${cols}-${rows}`}
|
||||
ref={gridRef}
|
||||
className="absolute inset-0 grid z-0"
|
||||
style={{
|
||||
@@ -88,7 +95,6 @@ function TileGrid() {
|
||||
className="w-full h-full relative"
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
willChange: "transform",
|
||||
}}
|
||||
>
|
||||
{/* Front - Black */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user