8 Commits

Author SHA1 Message Date
e97f28d12d fix: made flipping animation quicker 2026-02-21 22:21:03 +05:30
857c889cb1 Removed unused imports 2026-02-21 20:05:04 +05:30
5062133524 few changes 2026-02-21 19:51:12 +05:30
243ab9e073 Changed the navbar 2026-02-21 19:16:48 +05:30
63d3a88ae5 Adjusted the Marquee Speed 2026-02-21 18:36:52 +05:30
7cb4de4fcc Fixed the color in the footer 2026-02-21 15:27:18 +05:30
58394b4236 Fixed the Jerks of the Marquee effect 2026-02-21 15:19:04 +05:30
29fc82c104 Updated Frontend 2026-02-21 15:02:57 +05:30
22 changed files with 6571 additions and 101 deletions

268
app/(pages)/about/page.tsx Normal file
View File

@@ -0,0 +1,268 @@
"use client";
import { motion } from "framer-motion";
import { Diamond, Users, Trophy, Palette, Heart } from "lucide-react";
import Link from "next/link";
const stats = [
{ number: "500+", label: "Expected Participants" },
{ number: "3", label: "Categories" },
{ number: "₹50K+", label: "Prize Pool" },
{ number: "1", label: "Week of Creativity" },
];
const values = [
{
icon: Palette,
title: "Creativity",
description: "We celebrate original thinking and artistic expression in all its forms.",
},
{
icon: Users,
title: "Community",
description: "Building connections between artists and art enthusiasts across SJEC.",
},
{
icon: Trophy,
title: "Excellence",
description: "Recognizing and rewarding outstanding artistic talent and dedication.",
},
{
icon: Heart,
title: "Inclusivity",
description: "Welcoming artists of all skill levels and artistic backgrounds.",
},
];
const timeline = [
{ date: "Feb 15, 2026", event: "Registration Opens", status: "completed" },
{ date: "Feb 21, 2026", event: "Submissions Begin", status: "active" },
{ date: "Feb 28, 2026", event: "Submission Deadline", status: "upcoming" },
{ date: "Mar 1-5, 2026", event: "Public Voting", status: "upcoming" },
{ date: "Mar 7, 2026", event: "Winners Announced", status: "upcoming" },
];
export default function AboutPage() {
return (
<main className="min-h-screen bg-black pt-32 pb-20">
{/* Hero Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
ABOUT US
</span>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-white mb-6">
Celebrating Art at
<span className="text-lime-400"> SJEC</span>
</h1>
<p className="text-zinc-400 text-lg max-w-3xl mx-auto leading-relaxed">
ArtSplash is St. Joseph Engineering College's premier art competition,
brought to you by <span className="text-lime-400 font-semibold">Sceptix</span> —
the official technical club. We're on a mission to discover, celebrate,
and showcase the incredible artistic talent within our community.
</p>
</motion.div>
</section>
{/* Stats Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="grid grid-cols-2 md:grid-cols-4 gap-6"
>
{stats.map((stat, index) => (
<div
key={index}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 text-center"
>
<div className="text-3xl sm:text-4xl font-black text-lime-400 mb-2">
{stat.number}
</div>
<div className="text-zinc-400 text-sm">{stat.label}</div>
</div>
))}
</motion.div>
</section>
{/* Mission Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-20">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="inline-block bg-zinc-800 text-lime-400 px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
OUR MISSION
</span>
<h2 className="text-3xl sm:text-4xl font-black text-white mb-6">
Empowering Artists,
<br />
<span className="text-lime-400">Inspiring Creativity</span>
</h2>
<p className="text-zinc-400 leading-relaxed mb-6">
ArtSplash was created to provide a platform where students can express
their creativity, gain recognition for their work, and connect with
fellow artists. Whether you're a digital artist, traditional painter,
or poster designer, there's a place for you here.
</p>
<p className="text-zinc-400 leading-relaxed">
Our competition is open to all SJEC students who want to showcase their
artistic talents. The public voting system ensures that the community
has a voice in celebrating the best artworks.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="grid grid-cols-2 gap-4"
>
{values.map((value, index) => (
<div
key={index}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 hover:border-lime-400/50 transition-colors"
>
<div className="w-12 h-12 rounded-xl bg-lime-400/10 flex items-center justify-center mb-4">
<value.icon className="w-6 h-6 text-lime-400" />
</div>
<h3 className="text-white font-bold mb-2">{value.title}</h3>
<p className="text-zinc-500 text-sm">{value.description}</p>
</div>
))}
</motion.div>
</div>
</section>
{/* Timeline Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-20">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<span className="inline-block bg-zinc-800 text-lime-400 px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
EVENT TIMELINE
</span>
<h2 className="text-3xl sm:text-4xl font-black text-white">
Important <span className="text-lime-400">Dates</span>
</h2>
</motion.div>
<div className="relative">
{/* Timeline Line */}
<div className="absolute left-1/2 top-0 bottom-0 w-px bg-zinc-800 hidden md:block" />
<div className="space-y-8">
{timeline.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className={`flex flex-col md:flex-row items-center gap-4 ${
index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse"
}`}
>
<div className={`flex-1 ${index % 2 === 0 ? "md:text-right" : "md:text-left"}`}>
<div
className={`inline-block bg-zinc-900 border rounded-2xl p-6 ${
item.status === "active"
? "border-lime-400"
: item.status === "completed"
? "border-zinc-700"
: "border-zinc-800"
}`}
>
<div className="text-lime-400 text-sm font-bold mb-1">{item.date}</div>
<div className="text-white font-semibold">{item.event}</div>
<div
className={`text-xs mt-2 px-2 py-1 rounded-full inline-block ${
item.status === "active"
? "bg-lime-400/20 text-lime-400"
: item.status === "completed"
? "bg-zinc-700 text-zinc-400"
: "bg-zinc-800 text-zinc-500"
}`}
>
{item.status === "active"
? "In Progress"
: item.status === "completed"
? "Completed"
: "Upcoming"}
</div>
</div>
</div>
{/* Center Dot */}
<div
className={`w-4 h-4 rounded-full border-4 ${
item.status === "active"
? "bg-lime-400 border-lime-400/30"
: item.status === "completed"
? "bg-zinc-600 border-zinc-700"
: "bg-zinc-800 border-zinc-700"
}`}
/>
<div className="flex-1 hidden md:block" />
</motion.div>
))}
</div>
</div>
</section>
{/* Organized By Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-zinc-900 border border-zinc-800 rounded-3xl p-8 md:p-12 text-center"
>
<span className="inline-block bg-lime-400/10 text-lime-400 px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
ORGANIZED BY
</span>
<div className="flex items-center justify-center gap-3 mb-6">
<Diamond className="w-10 h-10 text-lime-400" />
<span className="text-3xl font-black text-white">SCEPTIX</span>
</div>
<p className="text-zinc-400 max-w-2xl mx-auto mb-8">
Sceptix is the official technical club of St. Joseph Engineering College,
dedicated to fostering innovation, creativity, and technical excellence
among students.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-8 py-4 text-sm font-bold tracking-wider hover:bg-lime-300 transition-all rounded-full"
>
JOIN ARTSPLASH
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-8 py-4 text-sm font-bold tracking-wider border border-zinc-700 hover:border-zinc-500 transition-all rounded-full"
>
CONTACT US
</Link>
</div>
</motion.div>
</section>
</main>
);
}

View File

@@ -0,0 +1,360 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import {
Mail,
Phone,
MapPin,
Send,
Instagram,
Linkedin,
MessageSquare,
Clock,
CheckCircle
} from "lucide-react";
import Link from "next/link";
const contactMethods = [
{
icon: Mail,
label: "Email",
value: "artsplash@sjec.ac.in",
href: "mailto:artsplash@sjec.ac.in",
description: "Best for detailed inquiries",
},
{
icon: Phone,
label: "Phone",
value: "+91 98765 43210",
href: "tel:+919876543210",
description: "Mon-Fri, 10AM-5PM",
},
{
icon: MapPin,
label: "Location",
value: "SJEC, Mangalore",
href: "https://maps.google.com/?q=St+Joseph+Engineering+College+Mangalore",
description: "Visit us on campus",
},
];
const socialLinks = [
{
icon: Instagram,
label: "Instagram",
href: "https://instagram.com/sceptix_sjec",
username: "@sceptix_sjec",
},
{
icon: Linkedin,
label: "LinkedIn",
href: "https://linkedin.com/company/sceptix-sjec",
username: "Sceptix SJEC",
},
];
const departments = [
"General Inquiry",
"Registration Help",
"Technical Support",
"Submission Issues",
"Voting Questions",
"Partnership & Sponsorship",
"Other",
];
export default function ContactPage() {
const [formState, setFormState] = useState({
name: "",
email: "",
department: "",
subject: "",
message: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormState((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setIsSubmitted(true);
};
if (isSubmitted) {
return (
<main className="min-h-screen bg-black pt-32 pb-20 flex items-center">
<div className="max-w-xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<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">
Message Sent!
</h1>
<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>
<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
</Link>
</motion.div>
</div>
</main>
);
}
return (
<main className="min-h-screen bg-black pt-32 pb-20">
<div className="max-w-6xl 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-16"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
CONTACT
</span>
<h1 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Get in <span className="text-lime-400">Touch</span>
</h1>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Have a question about ArtSplash? We're here to help. Reach out to us
through any of the channels below.
</p>
</motion.div>
<div className="grid lg:grid-cols-5 gap-8 lg:gap-12">
{/* Contact Info Column */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="lg:col-span-2 space-y-6"
>
{/* Contact Methods */}
<div className="space-y-4">
{contactMethods.map((method, index) => (
<a
key={method.label}
href={method.href}
target={method.label === "Location" ? "_blank" : undefined}
rel={method.label === "Location" ? "noopener noreferrer" : undefined}
className="block bg-zinc-900 border border-zinc-800 rounded-xl p-5 hover:border-zinc-700 transition-colors group"
>
<div className="flex items-start gap-4">
<div className="p-3 bg-lime-400/10 rounded-lg group-hover:bg-lime-400/20 transition-colors">
<method.icon className="w-6 h-6 text-lime-400" />
</div>
<div>
<p className="text-zinc-500 text-sm mb-1">{method.label}</p>
<p className="text-white font-semibold">{method.value}</p>
<p className="text-zinc-500 text-sm mt-1">{method.description}</p>
</div>
</div>
</a>
))}
</div>
{/* Response Time */}
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-lime-400/10 rounded-lg">
<Clock className="w-5 h-5 text-lime-400" />
</div>
<h3 className="text-white font-semibold">Response Time</h3>
</div>
<p className="text-zinc-400 text-sm">
We typically respond within <span className="text-lime-400 font-semibold">24-48 hours</span> on
business days. For urgent issues during the competition, we'll prioritize your query.
</p>
</div>
{/* Social Links */}
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
<h3 className="text-white font-semibold mb-4">Follow Us</h3>
<div className="flex gap-3">
{socialLinks.map((social) => (
<a
key={social.label}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 bg-zinc-800 text-zinc-300 px-4 py-3 rounded-xl hover:bg-zinc-700 hover:text-white transition-colors"
>
<social.icon className="w-5 h-5" />
<span className="text-sm font-medium">{social.username}</span>
</a>
))}
</div>
</div>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="lg:col-span-3"
>
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 sm:p-8">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-lime-400/10 rounded-lg">
<MessageSquare className="w-5 h-5 text-lime-400" />
</div>
<h2 className="text-xl font-bold text-white">Send us a Message</h2>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Name & Email Row */}
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label htmlFor="name" className="block text-sm font-medium text-zinc-300 mb-2">
Your Name <span className="text-lime-400">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formState.name}
onChange={handleChange}
required
placeholder="John Doe"
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"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-zinc-300 mb-2">
Email Address <span className="text-lime-400">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formState.email}
onChange={handleChange}
required
placeholder="john@example.com"
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"
/>
</div>
</div>
{/* Department & Subject Row */}
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label htmlFor="department" className="block text-sm font-medium text-zinc-300 mb-2">
Topic <span className="text-lime-400">*</span>
</label>
<div className="relative">
<select
id="department"
name="department"
value={formState.department}
onChange={handleChange}
required
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-white appearance-none focus:outline-none focus:border-lime-400 transition-colors cursor-pointer"
>
<option value="">Select a topic</option>
{departments.map((dept) => (
<option key={dept} value={dept}>{dept}</option>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
<svg className="w-5 h-5 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-zinc-300 mb-2">
Subject <span className="text-lime-400">*</span>
</label>
<input
type="text"
id="subject"
name="subject"
value={formState.subject}
onChange={handleChange}
required
placeholder="Brief subject"
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"
/>
</div>
</div>
{/* Message */}
<div>
<label htmlFor="message" className="block text-sm font-medium text-zinc-300 mb-2">
Your Message <span className="text-lime-400">*</span>
</label>
<textarea
id="message"
name="message"
value={formState.message}
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"
/>
<p className="text-zinc-500 text-sm mt-2">
{formState.message.length}/1000 characters
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className={`w-full flex items-center justify-center gap-2 px-6 py-4 rounded-xl font-bold transition-all ${
isSubmitting
? "bg-zinc-700 text-zinc-400 cursor-not-allowed"
: "bg-lime-400 text-black hover:bg-lime-300"
}`}
>
{isSubmitting ? (
<>
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Sending...
</>
) : (
<>
<Send className="w-5 h-5" />
Send Message
</>
)}
</button>
</form>
</div>
</motion.div>
</div>
</div>
</main>
);
}

296
app/(pages)/faq/page.tsx Normal file
View File

@@ -0,0 +1,296 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Link from "next/link";
import {
ChevronDown,
HelpCircle,
Vote,
Upload,
Award,
Shield,
Clock,
Mail
} from "lucide-react";
const faqs = [
{
category: "General",
icon: HelpCircle,
questions: [
{
q: "What is ArtSplash?",
a: "ArtSplash is SJEC's premier art competition organized by Sceptix. It's a platform for students to showcase their artistic talents across three categories: Digital Art, Paintings, and Poster Making. The competition features public voting and exciting prizes for winners.",
},
{
q: "When does the competition take place?",
a: "The competition runs from February 15 to March 7, 2026. This includes the registration period, submission window, and voting phase. Make sure to check the Rules page for specific deadlines for each phase.",
},
{
q: "Who can participate in ArtSplash?",
a: "ArtSplash is open to all students of St Joseph Engineering College (SJEC), Mangalore. You must have a valid SJEC email address (@sjec.ac.in) to register and submit artwork.",
},
],
},
{
category: "Registration & Submission",
icon: Upload,
questions: [
{
q: "How do I register for the competition?",
a: "Click on the 'Register' button and sign in using your SJEC Google account (@sjec.ac.in). Fill in your details including department, year, and phone number. Once registered, you can submit your artwork through the Submit page.",
},
{
q: "What file formats are accepted?",
a: "We accept JPG, PNG, and PDF formats. For Digital Art and Paintings, JPG or PNG is recommended. For Poster Making, PDF format is preferred. Maximum file size is 10MB.",
},
{
q: "Can I submit multiple artworks?",
a: "You can submit one artwork per category, meaning you can have up to 3 submissions total (one Digital Art, one Painting, and one Poster). Each submission must be original work.",
},
{
q: "Can I edit my submission after uploading?",
a: "You can edit your submission during the submission window. Once the voting phase begins, no changes can be made to ensure fairness. Make sure your submission is final before the deadline.",
},
],
},
{
category: "Voting",
icon: Vote,
questions: [
{
q: "Who can vote?",
a: "Anyone can vote! Unlike submissions which require an SJEC email, voting is open to everyone. Each person can cast one vote per email address, verified through our CAPTCHA system.",
},
{
q: "Can I see how many votes an artwork has?",
a: "To maintain fairness and prevent bandwagon voting, vote counts are hidden during the voting period. Results will be announced after the voting phase ends.",
},
{
q: "How is the winner decided?",
a: "Winners are determined by a combination of public votes (70%) and judge scores (30%). Our panel of judges evaluates artworks based on creativity, technique, and adherence to theme.",
},
],
},
{
category: "Rules & Guidelines",
icon: Shield,
questions: [
{
q: "What are the content guidelines?",
a: "All submissions must be original work created by the participant. AI-generated content is strictly prohibited and will be detected through our verification system. Content must be appropriate and not contain any offensive, violent, or copyrighted material.",
},
{
q: "What happens if I'm caught using AI-generated art?",
a: "All submissions go through our AI detection system. If AI-generated content is detected, your submission will be disqualified immediately, and you may be banned from future competitions.",
},
{
q: "Can I use references for my artwork?",
a: "Yes, you can use references for inspiration, but your final artwork must be substantially original. Direct copies or heavy traces of existing artwork are not allowed.",
},
],
},
{
category: "Prizes & Awards",
icon: Award,
questions: [
{
q: "What are the prizes?",
a: "Each category has three prize tiers: 1st Place, 2nd Place, and 3rd Place. Prizes include certificates, merchandise, and exciting rewards. Details will be announced on our social media channels.",
},
{
q: "When will winners be announced?",
a: "Winners will be announced within 3 days after the voting phase ends. All participants will be notified via email, and results will be posted on our website and social media.",
},
],
},
{
category: "Technical Issues",
icon: Clock,
questions: [
{
q: "My submission failed to upload. What should I do?",
a: "First, check if your file meets the requirements (JPG/PNG/PDF, under 10MB). Try using a different browser or clearing your cache. If the issue persists, contact us at artsplash@sjec.ac.in with details of the error.",
},
{
q: "I didn't receive a confirmation email after registering.",
a: "Check your spam/junk folder first. If you still can't find it, try logging in to verify your registration status. Contact us if you're still having issues.",
},
{
q: "Can I participate if I'm not currently on campus?",
a: "Absolutely! ArtSplash is entirely online. You can register, submit, and vote from anywhere as long as you have internet access and (for submissions) a valid SJEC email.",
},
],
},
];
export default function FAQPage() {
const [openItems, setOpenItems] = useState<Set<string>>(new Set(["general-0"]));
const [activeCategory, setActiveCategory] = useState("General");
const toggleItem = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const filteredFaqs = activeCategory === "All"
? faqs
: faqs.filter(cat => cat.category === activeCategory);
return (
<main className="min-h-screen bg-black pt-32 pb-20">
<div className="max-w-4xl 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">
FAQ
</span>
<h1 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Frequently Asked <span className="text-lime-400">Questions</span>
</h1>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Got questions? We've got answers. Find everything you need to know about ArtSplash below.
</p>
</motion.div>
{/* Category Tabs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="mb-8 overflow-x-auto scrollbar-hide"
>
<div className="flex gap-2 min-w-max pb-2">
<button
onClick={() => setActiveCategory("All")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap ${
activeCategory === "All"
? "bg-lime-400 text-black"
: "bg-zinc-900 text-zinc-400 hover:text-white border border-zinc-800"
}`}
>
All Topics
</button>
{faqs.map((cat) => (
<button
key={cat.category}
onClick={() => setActiveCategory(cat.category)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap ${
activeCategory === cat.category
? "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.category}
</button>
))}
</div>
</motion.div>
{/* FAQ Accordion */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="space-y-8"
>
{filteredFaqs.map((category, catIndex) => (
<div key={category.category}>
{/* Category Header */}
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-lime-400/10 rounded-lg">
<category.icon className="w-5 h-5 text-lime-400" />
</div>
<h2 className="text-xl font-bold text-white">{category.category}</h2>
</div>
{/* Questions */}
<div className="space-y-3">
{category.questions.map((item, qIndex) => {
const itemId = `${category.category.toLowerCase().replace(/\s+/g, "-")}-${qIndex}`;
const isOpen = openItems.has(itemId);
return (
<motion.div
key={qIndex}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: qIndex * 0.05 }}
className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden"
>
<button
onClick={() => toggleItem(itemId)}
className="w-full flex items-center justify-between p-5 text-left"
>
<span className="text-white font-semibold pr-4">{item.q}</span>
<ChevronDown
className={`w-5 h-5 text-zinc-400 flex-shrink-0 transition-transform duration-300 ${
isOpen ? "rotate-180" : ""
}`}
/>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="px-5 pb-5">
<div className="h-px bg-zinc-800 mb-4" />
<p className="text-zinc-400 leading-relaxed">{item.a}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
</div>
))}
</motion.div>
{/* Contact CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="mt-16 text-center"
>
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-8">
<div className="w-14 h-14 rounded-full bg-lime-400/10 flex items-center justify-center mx-auto mb-4">
<Mail className="w-7 h-7 text-lime-400" />
</div>
<h3 className="text-xl font-bold text-white mb-2">Still have questions?</h3>
<p className="text-zinc-400 mb-6">
Can't find what you're looking for? We're here to help!
</p>
<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
</Link>
</div>
</motion.div>
</div>
</main>
);
}

View File

@@ -0,0 +1,503 @@
"use client";
import { useState, useMemo, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
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>>(() => {
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();
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 = 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">
<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)}
>
<Image
src={artwork.imageUrl}
alt={artwork.title}
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" />
</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">
<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"
>
<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>
);
}

16
app/(pages)/layout.tsx Normal file
View File

@@ -0,0 +1,16 @@
import Navbar from "../components/Navbar";
import Footer from "../components/Footer";
export default function PagesLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
}

View File

@@ -0,0 +1,395 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import {
Mail,
User,
Phone,
GraduationCap,
CheckCircle,
AlertCircle,
ArrowRight,
Chrome
} from "lucide-react";
import Link from "next/link";
const departments = [
"Computer Science & Engineering",
"Information Science & Engineering",
"Electronics & Communication Engineering",
"Electrical & Electronics Engineering",
"Mechanical Engineering",
"Civil Engineering",
"Artificial Intelligence & Machine Learning",
"Data Science",
];
const years = ["1st Year", "2nd Year", "3rd Year", "4th Year"];
export default function RegisterPage() {
const [formData, setFormData] = useState({
name: "",
email: "",
phone: "",
department: "",
year: "",
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 });
if (email.length > 0) {
setIsEmailValid(validateEmail(email));
} else {
setIsEmailValid(null);
}
};
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 (!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 */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6 }}
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
JOIN ARTSPLASH
</span>
<h1 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-6">
Register to
<span className="text-lime-400"> Compete</span>
</h1>
<p className="text-zinc-400 text-lg mb-8 leading-relaxed">
Create your account to participate in ArtSplash 2026.
Only SJEC students with valid college email addresses can register to submit artwork.
</p>
{/* Benefits */}
<div className="space-y-4 mb-8">
{[
"Submit artwork in up to 3 categories",
"Get your art seen by hundreds of voters",
"Compete for exciting prizes",
"Receive feedback from moderators",
].map((benefit, index) => (
<div key={index} className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-lime-400" />
<span className="text-zinc-300">{benefit}</span>
</div>
))}
</div>
{/* Important Notice */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-lime-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-white font-semibold mb-2">Important</h3>
<p className="text-zinc-400 text-sm">
You must use your official SJEC email address (@sjec.ac.in) to register
and submit artwork. Public voters can use any email address.
</p>
</div>
</div>
</div>
</motion.div>
{/* Right Column - Form */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl p-8">
{/* OAuth Button */}
<button
type="button"
className="w-full flex items-center justify-center gap-3 bg-white text-black px-6 py-4 rounded-xl font-semibold hover:bg-zinc-100 transition-colors mb-6"
>
<Chrome className="w-5 h-5" />
Continue with Google
</button>
<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-700" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-zinc-900 text-zinc-500">or register with email</span>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Name Field */}
<div>
<label className="block text-white text-sm font-medium mb-2">
Full Name
</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" />
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: 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"
placeholder="Enter your full name"
/>
</div>
</div>
{/* Email Field */}
<div>
<label className="block text-white text-sm font-medium mb-2">
SJEC Email Address
</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" />
<input
type="email"
required
value={formData.email}
onChange={handleEmailChange}
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 ${
isEmailValid === null
? "border-zinc-700 focus:border-lime-400"
: isEmailValid
? "border-lime-400"
: "border-red-500"
}`}
placeholder="yourname@sjec.ac.in"
/>
{isEmailValid !== null && (
<div className="absolute right-4 top-1/2 -translate-y-1/2">
{isEmailValid ? (
<CheckCircle className="w-5 h-5 text-lime-400" />
) : (
<AlertCircle className="w-5 h-5 text-red-500" />
)}
</div>
)}
</div>
{isEmailValid === false && (
<p className="text-red-500 text-sm mt-2">
Please use your SJEC email address (@sjec.ac.in)
</p>
)}
</div>
{/* Phone Field */}
<div>
<label className="block text-white text-sm font-medium mb-2">
Phone Number
</label>
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" />
<input
type="tel"
required
value={formData.phone}
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 */}
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="block text-white text-sm font-medium mb-2">
Department
</label>
<div className="relative">
<GraduationCap className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" />
<select
required
value={formData.department}
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl pl-12 pr-4 py-3 text-white appearance-none focus:outline-none focus:border-lime-400 transition-colors"
>
<option value="">Select</option>
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-white text-sm font-medium mb-2">
Year
</label>
<select
required
value={formData.year}
onChange={(e) => setFormData({ ...formData, year: e.target.value })}
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-white appearance-none focus:outline-none focus:border-lime-400 transition-colors"
>
<option value="">Select Year</option>
{years.map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
</div>
{/* Terms Checkbox */}
<div className="flex items-start gap-3">
<input
type="checkbox"
id="agreeToRules"
required
checked={formData.agreeToRules}
onChange={(e) => setFormData({ ...formData, agreeToRules: e.target.checked })}
className="mt-1 w-4 h-4 rounded border-zinc-700 bg-zinc-800 text-lime-400 focus:ring-lime-400"
/>
<label htmlFor="agreeToRules" className="text-zinc-400 text-sm">
I have read and agree to the{" "}
<Link href="/rules" className="text-lime-400 hover:underline">
rules and guidelines
</Link>{" "}
of ArtSplash 2026
</label>
</div>
{/* Submit Button */}
<button
type="submit"
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 ? (
<>
<div className="w-5 h-5 border-2 border-black/30 border-t-black rounded-full animate-spin" />
REGISTERING...
</>
) : (
<>
CREATE ACCOUNT
<ArrowRight className="w-5 h-5" />
</>
)}
</button>
</form>
<p className="text-center text-zinc-500 text-sm mt-6">
Already have an account?{" "}
<Link href="/login" className="text-lime-400 hover:underline">
Sign in
</Link>
</p>
</div>
</motion.div>
</div>
</div>
)}
</main>
);
}

312
app/(pages)/rules/page.tsx Normal file
View File

@@ -0,0 +1,312 @@
"use client";
import { motion } from "framer-motion";
import {
CheckCircle,
XCircle,
AlertTriangle,
FileImage,
Users,
Vote,
Award,
Clock,
Mail
} from "lucide-react";
const eligibilityRules = [
{ text: "Must be a current student of St. Joseph Engineering College (SJEC)", allowed: true },
{ text: "Must have a valid SJEC email address (@sjec.ac.in) to submit artwork", allowed: true },
{ text: "One submission per category per participant", allowed: true },
{ text: "Participants can enter multiple categories", allowed: true },
{ text: "Alumni and faculty are not eligible to participate", allowed: false },
{ text: "Group submissions are not allowed", allowed: false },
];
const submissionRules = [
{
icon: FileImage,
title: "File Requirements",
rules: [
"Accepted formats: JPG, PNG, PDF",
"Maximum file size: 10MB",
"Minimum resolution: 1920x1080 pixels",
"Digital art must be in RGB color mode",
],
},
{
icon: CheckCircle,
title: "Content Guidelines",
rules: [
"All artwork must be original and created by the participant",
"No AI-generated artwork will be accepted",
"Artwork must not contain offensive, inappropriate, or copyrighted content",
"Previously published work may be submitted if you own the rights",
],
},
{
icon: Clock,
title: "Submission Details",
rules: [
"Include a title (max 100 characters)",
"Provide a description of your artwork (max 500 characters)",
"Select the appropriate category",
"Fill in accurate artist details",
],
},
];
const categoryRules = [
{
title: "Digital Art",
description: "Artwork created using digital tools and software",
requirements: [
"Created using software like Photoshop, Illustrator, Procreate, etc.",
"Digital paintings, illustrations, and photo manipulations accepted",
"Must be entirely digitally created (no scanned traditional art)",
"3D renders and digital sculptures are accepted",
],
},
{
title: "Paintings",
description: "Traditional paintings in any medium",
requirements: [
"Oil, acrylic, watercolor, or any traditional painting medium",
"Submit high-quality photographs or scans of your work",
"Physical artwork must be completed before submission deadline",
"Mixed media paintings are accepted",
],
},
{
title: "Poster Making",
description: "Creative posters with impactful messages",
requirements: [
"Can be digital or traditional",
"Must have a clear theme or message",
"Text and typography should be legible",
"Theme: Open (artist's choice)",
],
},
];
const votingRules = [
{ text: "Voting is open to the general public", icon: Users },
{ text: "One vote per email address per artwork", icon: Vote },
{ text: "CAPTCHA verification required to prevent spam", icon: AlertTriangle },
{ text: "Vote counts will be hidden during the voting period", icon: Award },
{ text: "Voting period: March 1-5, 2026", icon: Clock },
{ text: "Results will be announced via email", icon: Mail },
];
const disqualificationReasons = [
"Submission of AI-generated artwork",
"Plagiarism or copyright infringement",
"Submitting work created by someone else",
"Multiple submissions in the same category",
"Inappropriate or offensive content",
"Vote manipulation or fraudulent voting",
"Missing or false participant information",
];
export default function RulesPage() {
return (
<main className="min-h-screen bg-black pt-32 pb-20">
{/* Hero Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-6">
COMPETITION RULES
</span>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-white mb-6">
Rules & <span className="text-lime-400">Guidelines</span>
</h1>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Please read all rules carefully before participating. Violation of any
rules may result in disqualification.
</p>
</motion.div>
</section>
{/* Eligibility Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-2xl sm:text-3xl font-black text-white mb-8 flex items-center gap-3">
<Users className="w-8 h-8 text-lime-400" />
Eligibility
</h2>
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 md:p-8">
<div className="grid gap-4">
{eligibilityRules.map((rule, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 rounded-xl bg-zinc-800/50"
>
{rule.allowed ? (
<CheckCircle className="w-5 h-5 text-lime-400 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
)}
<span className="text-zinc-300">{rule.text}</span>
</div>
))}
</div>
</div>
</motion.div>
</section>
{/* Submission Rules Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-2xl sm:text-3xl font-black text-white mb-8 flex items-center gap-3">
<FileImage className="w-8 h-8 text-lime-400" />
Submission Rules
</h2>
<div className="grid md:grid-cols-3 gap-6">
{submissionRules.map((section, index) => (
<div
key={index}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6"
>
<div className="w-12 h-12 rounded-xl bg-lime-400/10 flex items-center justify-center mb-4">
<section.icon className="w-6 h-6 text-lime-400" />
</div>
<h3 className="text-white font-bold text-lg mb-4">{section.title}</h3>
<ul className="space-y-3">
{section.rules.map((rule, ruleIndex) => (
<li key={ruleIndex} className="flex items-start gap-2 text-zinc-400 text-sm">
<span className="text-lime-400 mt-1"></span>
{rule}
</li>
))}
</ul>
</div>
))}
</div>
</motion.div>
</section>
{/* Category Rules Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-2xl sm:text-3xl font-black text-white mb-8 flex items-center gap-3">
<Award className="w-8 h-8 text-lime-400" />
Category Guidelines
</h2>
<div className="space-y-6">
{categoryRules.map((category, index) => (
<div
key={index}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 md:p-8"
>
<div className="flex flex-col md:flex-row md:items-start gap-6">
<div className="flex-1">
<h3 className="text-xl font-bold text-lime-400 mb-2">
{category.title}
</h3>
<p className="text-zinc-400 mb-4">{category.description}</p>
</div>
<div className="flex-1">
<h4 className="text-white font-semibold mb-3">Requirements:</h4>
<ul className="space-y-2">
{category.requirements.map((req, reqIndex) => (
<li key={reqIndex} className="flex items-start gap-2 text-zinc-400 text-sm">
<CheckCircle className="w-4 h-4 text-lime-400 flex-shrink-0 mt-0.5" />
{req}
</li>
))}
</ul>
</div>
</div>
</div>
))}
</div>
</motion.div>
</section>
{/* Voting Rules Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-2xl sm:text-3xl font-black text-white mb-8 flex items-center gap-3">
<Vote className="w-8 h-8 text-lime-400" />
Voting Rules
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{votingRules.map((rule, index) => (
<div
key={index}
className="bg-zinc-900 border border-zinc-800 rounded-xl p-4 flex items-center gap-4"
>
<div className="w-10 h-10 rounded-lg bg-lime-400/10 flex items-center justify-center flex-shrink-0">
<rule.icon className="w-5 h-5 text-lime-400" />
</div>
<span className="text-zinc-300 text-sm">{rule.text}</span>
</div>
))}
</div>
</motion.div>
</section>
{/* Disqualification Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-2xl sm:text-3xl font-black text-white mb-8 flex items-center gap-3">
<AlertTriangle className="w-8 h-8 text-red-500" />
Disqualification Criteria
</h2>
<div className="bg-red-500/10 border border-red-500/30 rounded-2xl p-6 md:p-8">
<p className="text-zinc-300 mb-6">
Participants may be disqualified for any of the following reasons:
</p>
<div className="grid sm:grid-cols-2 gap-3">
{disqualificationReasons.map((reason, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 rounded-xl bg-zinc-900/50"
>
<XCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-400 text-sm">{reason}</span>
</div>
))}
</div>
<div className="mt-6 p-4 bg-zinc-900 rounded-xl">
<p className="text-zinc-400 text-sm">
<strong className="text-white">Note:</strong> All submissions will be
reviewed by moderators using AI detection tools and manual inspection.
Decisions made by the organizing committee are final.
</p>
</div>
</div>
</motion.div>
</section>
</main>
);
}

390
app/(pages)/submit/page.tsx Normal file
View File

@@ -0,0 +1,390 @@
"use client";
import { useState, useRef } from "react";
import { motion } from "framer-motion";
import {
Upload,
FileText,
X,
CheckCircle,
AlertCircle,
Monitor,
Palette,
FileImage,
ArrowRight,
Info
} from "lucide-react";
import Link from "next/link";
const categories = [
{
id: "digital-art",
title: "Digital Art",
icon: Monitor,
description: "Artwork created using digital tools and software",
},
{
id: "paintings",
title: "Paintings",
icon: Palette,
description: "Traditional paintings in any medium",
},
{
id: "poster-making",
title: "Poster Making",
icon: FileImage,
description: "Creative posters with impactful messages",
},
];
export default function SubmitPage() {
const [formData, setFormData] = useState({
title: "",
description: "",
category: "",
});
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);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = (selectedFile: File) => {
const validTypes = ["image/jpeg", "image/png", "application/pdf"];
const maxSize = 10 * 1024 * 1024; // 10MB
setFileError(null);
if (!validTypes.includes(selectedFile.type)) {
setFileError("Invalid file type. Please upload a JPG, PNG, or PDF file.");
return;
}
if (selectedFile.size > maxSize) {
setFileError("File too large. Maximum size is 10MB.");
return;
}
setFile(selectedFile);
if (selectedFile.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => setPreview(e.target?.result as string);
reader.readAsDataURL(selectedFile);
} else {
setPreview(null);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFile = e.dataTransfer.files[0];
if (droppedFile) handleFileSelect(droppedFile);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file || !formData.category) return;
setIsSubmitting(true);
// TODO: Implement actual submission logic
await new Promise(resolve => setTimeout(resolve, 2000));
setIsSubmitting(false);
setSubmitSuccess(true);
};
const removeFile = () => {
setFile(null);
setPreview(null);
setFileError(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
if (submitSuccess) {
return (
<main className="min-h-screen bg-black pt-32 pb-20 flex items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center px-4"
>
<div className="w-20 h-20 rounded-full bg-lime-400/20 flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-10 h-10 text-lime-400" />
</div>
<h1 className="text-3xl sm:text-4xl font-black text-white mb-4">
Submission Successful!
</h1>
<p className="text-zinc-400 mb-8 max-w-md mx-auto">
Your artwork has been submitted for review. You'll receive an email
notification once it's approved by our moderators.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={() => {
setSubmitSuccess(false);
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"
>
Submit Another
</button>
<Link
href="/gallery"
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"
>
View Gallery
</Link>
</div>
</motion.div>
</main>
);
}
return (
<main className="min-h-screen bg-black pt-32 pb-20">
<div className="max-w-4xl 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">
SUBMIT ARTWORK
</span>
<h1 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Share Your <span className="text-lime-400">Masterpiece</span>
</h1>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Upload your artwork and let the world see your creativity.
Make sure to read the rules before submitting.
</p>
</motion.div>
{/* Info Banner */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 mb-8 flex items-start gap-3"
>
<Info className="w-5 h-5 text-lime-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-zinc-400">
<strong className="text-white">Submission Requirements:</strong> JPG, PNG, or PDF files up to 10MB.
Minimum resolution 1920x1080. AI-generated artwork will be detected and rejected.
</div>
</motion.div>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Category Selection */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<label className="block text-white text-lg font-bold mb-4">
Select Category
</label>
<div className="grid sm:grid-cols-3 gap-4">
{categories.map((category) => (
<button
key={category.id}
type="button"
onClick={() => setFormData({ ...formData, category: category.id })}
className={`p-6 rounded-2xl border-2 transition-all text-left ${
formData.category === category.id
? "bg-lime-400/10 border-lime-400"
: "bg-zinc-900 border-zinc-800 hover:border-zinc-700"
}`}
>
<category.icon
className={`w-8 h-8 mb-3 ${
formData.category === category.id ? "text-lime-400" : "text-zinc-500"
}`}
/>
<h3 className={`font-bold mb-1 ${
formData.category === category.id ? "text-lime-400" : "text-white"
}`}>
{category.title}
</h3>
<p className="text-zinc-500 text-sm">{category.description}</p>
</button>
))}
</div>
</motion.div>
{/* File Upload */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<label className="block text-white text-lg font-bold mb-4">
Upload Artwork
</label>
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={`relative border-2 border-dashed rounded-2xl transition-all ${
isDragging
? "border-lime-400 bg-lime-400/5"
: file
? "border-lime-400/50 bg-zinc-900"
: "border-zinc-700 bg-zinc-900 hover:border-zinc-600"
}`}
>
<input
ref={fileInputRef}
type="file"
accept=".jpg,.jpeg,.png,.pdf"
onChange={(e) => e.target.files?.[0] && handleFileSelect(e.target.files[0])}
className="hidden"
/>
{file ? (
<div className="p-6">
<div className="flex items-start gap-4">
{preview ? (
<img
src={preview}
alt="Preview"
className="w-32 h-32 object-cover rounded-xl"
/>
) : (
<div className="w-32 h-32 bg-zinc-800 rounded-xl flex items-center justify-center">
<FileText className="w-12 h-12 text-zinc-600" />
</div>
)}
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<p className="text-white font-semibold mb-1">{file.name}</p>
<p className="text-zinc-500 text-sm">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
<button
type="button"
onClick={removeFile}
className="p-2 hover:bg-zinc-800 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-zinc-400" />
</button>
</div>
<div className="flex items-center gap-2 mt-3 text-lime-400 text-sm">
<CheckCircle className="w-4 h-4" />
File ready for upload
</div>
</div>
</div>
</div>
) : (
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="w-full p-12 text-center"
>
<div className="w-16 h-16 rounded-full bg-zinc-800 flex items-center justify-center mx-auto mb-4">
<Upload className="w-8 h-8 text-zinc-500" />
</div>
<p className="text-white font-semibold mb-2">
Drop your artwork here or click to browse
</p>
<p className="text-zinc-500 text-sm">
Supports JPG, PNG, PDF up to 10MB
</p>
</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 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="space-y-6"
>
<div>
<label className="block text-white text-lg font-bold mb-4">
Artwork Title
</label>
<input
type="text"
required
maxLength={100}
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-4 text-white placeholder-zinc-500 focus:outline-none focus:border-lime-400 transition-colors"
placeholder="Give your artwork a memorable title"
/>
<p className="text-zinc-500 text-sm mt-2">{formData.title.length}/100 characters</p>
</div>
<div>
<label className="block text-white text-lg font-bold mb-4">
Description
</label>
<textarea
required
maxLength={500}
rows={4}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-4 text-white placeholder-zinc-500 focus:outline-none focus:border-lime-400 transition-colors resize-none"
placeholder="Tell us about your artwork, the inspiration behind it, and the techniques used..."
/>
<p className="text-zinc-500 text-sm mt-2">{formData.description.length}/500 characters</p>
</div>
</motion.div>
{/* Submit Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4"
>
<button
type="submit"
disabled={isSubmitting || !file || !formData.category || !formData.title || !formData.description}
className="flex-1 flex items-center justify-center gap-2 bg-lime-400 text-black px-8 py-4 rounded-full font-bold tracking-wider hover:bg-lime-300 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<div className="w-5 h-5 border-2 border-black/30 border-t-black rounded-full animate-spin" />
SUBMITTING...
</>
) : (
<>
SUBMIT ARTWORK
<ArrowRight className="w-5 h-5" />
</>
)}
</button>
<Link
href="/rules"
className="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"
>
View Rules
</Link>
</motion.div>
</form>
</div>
</main>
);
}

69
app/components/CTA.tsx Normal file
View File

@@ -0,0 +1,69 @@
"use client";
import { motion } from "framer-motion";
import { ArrowRight, Sparkles } from "lucide-react";
import Link from "next/link";
export default function CTA() {
return (
<section className="py-24 bg-black relative overflow-hidden">
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<div className="inline-flex items-center gap-2 bg-lime-400 text-black px-4 py-2 text-xs font-bold tracking-wider rounded-full mb-8">
<Sparkles className="w-4 h-4" />
LIMITED TIME
</div>
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-white mb-6 leading-tight">
Ready to Showcase
<br />
<span className="text-lime-400">
Your Masterpiece?
</span>
</h2>
<p className="text-zinc-400 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
Join hundreds of talented artists from SJEC in this creative
celebration. Submit your artwork and let the world vote for your
talent.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-10 py-5 text-base font-bold tracking-wide hover:bg-lime-300 transition-all hover:scale-105 rounded-full shadow-xl shadow-lime-400/25"
>
REGISTER NOW
<ArrowRight className="w-5 h-5" />
</Link>
<Link
href="/rules"
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-10 py-5 text-base font-bold tracking-wide border-2 border-zinc-700 hover:border-lime-400 hover:text-lime-400 transition-all rounded-full"
>
VIEW RULES
</Link>
</div>
{/* Deadline Notice */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="mt-12 inline-flex items-center gap-3 bg-zinc-900 border border-zinc-800 px-6 py-3 rounded-full"
>
<span className="w-3 h-3 bg-red-500 rounded-full animate-pulse" />
<span className="text-zinc-400 font-medium">
Submissions close on <span className="text-lime-400 font-bold">February 28, 2026</span>
</span>
</motion.div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,280 @@
"use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { Palette, Monitor, FileImage, ArrowRight, LucideIcon } from "lucide-react";
import Link from "next/link";
import gsap from "gsap";
const categories = [
{
icon: Monitor,
title: "Digital Art",
description:
"Create stunning digital masterpieces using any software of your choice.",
color: "from-violet-500 to-purple-600",
bgColor: "bg-violet-500",
},
{
icon: Palette,
title: "Paintings",
description:
"Traditional paintings in any medium - oil, acrylic, watercolor.",
color: "from-orange-500 to-red-500",
bgColor: "bg-orange-500",
},
{
icon: FileImage,
title: "Poster Making",
description:
"Design impactful posters that communicate powerful messages.",
color: "from-lime-500 to-green-500",
bgColor: "bg-lime-500",
},
];
// BounceCards Component for Category Cards
interface CategoryCard {
icon: LucideIcon;
title: string;
description: string;
color: string;
bgColor: string;
}
interface BounceCardsProps {
className?: string;
cards: CategoryCard[];
containerWidth?: number;
containerHeight?: number;
animationDelay?: number;
animationStagger?: number;
easeType?: string;
transformStyles?: string[];
enableHover?: boolean;
}
function BounceCards({
className = "",
cards,
containerWidth = 500,
containerHeight = 400,
animationDelay = 0.5,
animationStagger = 0.08,
easeType = "elastic.out(1, 0.8)",
transformStyles = [
"rotate(-8deg) translate(-200px)",
"rotate(0deg)",
"rotate(8deg) translate(200px)",
],
enableHover = true,
}: BounceCardsProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.fromTo(
".card",
{ scale: 0 },
{
scale: 1,
stagger: animationStagger,
ease: easeType,
delay: animationDelay,
}
);
}, containerRef);
return () => ctx.revert();
}, [animationDelay, animationStagger, easeType]);
const getNoRotationTransform = (transformStr: string): string => {
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
if (hasRotate) {
return transformStr.replace(/rotate\([\s\S]*?\)/, "rotate(0deg)");
} else if (transformStr === "none") {
return "rotate(0deg)";
} else {
return `${transformStr} rotate(0deg)`;
}
};
const getPushedTransform = (
baseTransform: string,
offsetX: number
): string => {
const translateRegex = /translate\(([-0-9.]+)px\)/;
const match = baseTransform.match(translateRegex);
if (match) {
const currentX = parseFloat(match[1]);
const newX = currentX + offsetX;
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
} else {
return baseTransform === "none"
? `translate(${offsetX}px)`
: `${baseTransform} translate(${offsetX}px)`;
}
};
const pushSiblings = (hoveredIdx: number) => {
const q = gsap.utils.selector(containerRef);
if (!enableHover || !containerRef.current) return;
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
if (i === hoveredIdx) {
const noRotation = getNoRotationTransform(baseTransform);
gsap.to(selector, {
transform: noRotation,
zIndex: 10,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
} else {
const offsetX = i < hoveredIdx ? -80 : 80;
const pushedTransform = getPushedTransform(baseTransform, offsetX);
const distance = Math.abs(hoveredIdx - i);
const delay = distance * 0.05;
gsap.to(selector, {
transform: pushedTransform,
zIndex: 1,
duration: 0.4,
ease: "back.out(1.4)",
delay,
overwrite: "auto",
});
}
});
};
const resetSiblings = () => {
if (!enableHover || !containerRef.current) return;
const q = gsap.utils.selector(containerRef);
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
gsap.to(selector, {
transform: baseTransform,
zIndex: cards.length - i,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
});
};
return (
<div
className={`relative flex items-center justify-center ${className}`}
ref={containerRef}
style={{
width: containerWidth,
height: containerHeight,
}}
>
{cards.map((card, idx) => (
<div
key={idx}
className={`card card-${idx} absolute w-[220px] h-[280px] border-4 border-zinc-800 rounded-[24px] overflow-hidden bg-zinc-900 p-6 flex flex-col cursor-pointer`}
style={{
boxShadow: "0 8px 40px rgba(0, 0, 0, 0.5)",
transform: transformStyles[idx] || "none",
zIndex: cards.length - idx,
}}
onMouseEnter={() => pushSiblings(idx)}
onMouseLeave={resetSiblings}
>
{/* Icon */}
<div
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center mb-4 flex-shrink-0`}
>
<card.icon className="w-7 h-7 text-white" />
</div>
{/* Title */}
<h3 className="text-xl font-bold text-white mb-2">{card.title}</h3>
{/* Description */}
<p className="text-zinc-400 text-sm leading-relaxed flex-grow">
{card.description}
</p>
{/* Submit Link */}
<div className="mt-4 pt-4 border-t border-zinc-800">
<Link
href="/submit"
className="inline-flex items-center gap-2 text-lime-400 text-sm font-semibold hover:text-lime-300 transition-colors"
>
Submit Now
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
))}
</div>
);
}
export default function Categories() {
const transformStyles = [
"rotate(-8deg) translate(-220px)",
"rotate(0deg)",
"rotate(8deg) translate(220px)",
];
return (
<section className="py-24 bg-black overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-4">
CATEGORIES
</span>
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Three Ways to
<span className="text-lime-400"> Express</span>
</h2>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Choose your canvas, unleash your creativity. Each category offers a
unique way to showcase your artistic vision.
</p>
</motion.div>
{/* BounceCards with Category Cards */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
className="flex justify-center"
>
<BounceCards
cards={categories}
containerWidth={700}
containerHeight={380}
animationDelay={0.3}
animationStagger={0.1}
easeType="elastic.out(1, 0.8)"
transformStyles={transformStyles}
enableHover={true}
/>
</motion.div>
</div>
</section>
);
}

111
app/components/Footer.tsx Normal file
View File

@@ -0,0 +1,111 @@
"use client";
import Link from "next/link";
import { Diamond, Mail, Instagram, Twitter } from "lucide-react";
const footerLinks = {
competition: [
{ href: "/register", label: "Register" },
{ href: "/submit", label: "Submit Art" },
{ href: "/gallery", label: "Gallery" },
{ href: "/rules", label: "Rules" },
],
information: [
{ href: "/about", label: "About" },
{ href: "/faq", label: "FAQ" },
{ href: "/contact", label: "Contact" },
],
};
export default function Footer() {
return (
<footer className="bg-zinc-950 text-white border-t border-zinc-800">
{/* Main Footer */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
{/* Brand */}
<div className="lg:col-span-2">
<Link href="/" className="flex items-center gap-2 mb-6">
<Diamond className="w-8 h-8 text-lime-400" />
<span className="text-2xl font-black tracking-tight">ARTSPLASH</span>
</Link>
<p className="text-zinc-400 max-w-md mb-6 leading-relaxed">
ArtSplash is SJEC's premier art competition celebrating creativity
across Digital Art, Paintings, and Poster Making. Join us in this
vibrant celebration of artistic expression.
</p>
<div className="flex gap-4">
<a
href="#"
className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center hover:bg-lime-400 hover:text-black transition-colors"
>
<Instagram className="w-5 h-5" />
</a>
<a
href="#"
className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center hover:bg-lime-400 hover:text-black transition-colors"
>
<Twitter className="w-5 h-5" />
</a>
<a
href="mailto:artsplash@sjec.ac.in"
className="w-10 h-10 rounded-full bg-zinc-800 flex items-center justify-center hover:bg-lime-400 hover:text-black transition-colors"
>
<Mail className="w-5 h-5" />
</a>
</div>
</div>
{/* Competition Links */}
<div>
<h3 className="text-lg font-bold mb-4">Competition</h3>
<ul className="space-y-3">
{footerLinks.competition.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-zinc-400 hover:text-lime-400 transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Information Links */}
<div>
<h3 className="text-lg font-bold mb-4">Information</h3>
<ul className="space-y-3">
{footerLinks.information.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-zinc-400 hover:text-lime-400 transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-zinc-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-zinc-500 text-sm">
© 2026 ArtSplash. All rights reserved.
</p>
<div className="flex items-center gap-2 text-zinc-500 text-sm">
<span>Powered by</span>
<span className="text-lime-400 font-bold">Sceptix</span>
</div>
</div>
</div>
</div>
</footer>
);
}

233
app/components/Hero.tsx Normal file
View File

@@ -0,0 +1,233 @@
"use client";
import { useEffect, useRef, useCallback, useState } from "react";
import Link from "next/link";
import gsap from "gsap";
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) {
setGridSize({ cols: 8, rows: 10 });
} else if (width < 1024) {
setGridSize({ cols: 12, rows: 10 });
} else {
setGridSize({ cols: 20, rows: 12 });
}
};
const debouncedUpdate = () => {
clearTimeout(timeout);
timeout = setTimeout(updateGridSize, 150);
};
updateGridSize();
window.addEventListener("resize", debouncedUpdate);
return () => {
clearTimeout(timeout);
window.removeEventListener("resize", debouncedUpdate);
};
}, []);
return gridSize;
}
function TileGrid() {
const gridRef = useRef<HTMLDivElement>(null);
const tilesRef = useRef<(HTMLDivElement | null)[]>([]);
const { cols, rows } = useGridSize();
const handleMouseEnter = useCallback((index: number) => {
const tile = tilesRef.current[index];
if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, {
rotateY: 180,
duration: 0.06, // Reduced duration for quicker flip
ease: "power2.inOut",
overwrite: true,
});
}
}, []);
const handleMouseLeave = useCallback((index: number) => {
const tile = tilesRef.current[index];
if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, {
rotateY: 0,
duration: 0.6, // Reduced duration for quicker flip back
ease: "power2.inOut",
overwrite: true,
});
}
}, []);
// Reset refs array when grid size changes
useEffect(() => {
tilesRef.current = [];
}, [cols, rows]);
return (
<div
key={`grid-${cols}-${rows}`}
ref={gridRef}
className="absolute inset-0 grid z-0"
style={{
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gridTemplateRows: `repeat(${rows}, 1fr)`,
}}
>
{Array.from({ length: cols * rows }).map((_, index) => (
<div
key={index}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
className="cursor-pointer"
style={{ perspective: "500px" }}
>
<div
ref={(el) => { tilesRef.current[index] = el; }}
className="w-full h-full relative"
style={{
transformStyle: "preserve-3d",
}}
>
{/* Front - Black */}
<div
className="absolute inset-0 bg-black border border-zinc-900"
style={{ backfaceVisibility: "hidden" }}
/>
{/* Back - White */}
<div
className="absolute inset-0 bg-white"
style={{
backfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
/>
</div>
</div>
))}
</div>
);
}
export default function Hero() {
const headingRef = useRef<HTMLHeadingElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Animate heading on mount
if (headingRef.current) {
gsap.fromTo(
headingRef.current,
{ opacity: 0, y: 50 },
{ opacity: 1, y: 0, duration: 1, ease: "power3.out", delay: 0.3 }
);
}
if (contentRef.current) {
gsap.fromTo(
contentRef.current.children,
{ opacity: 0, y: 30 },
{ opacity: 1, y: 0, duration: 0.8, stagger: 0.15, ease: "power3.out", delay: 0.5 }
);
}
}, []);
return (
<section className="relative min-h-screen bg-black overflow-hidden pt-28">
{/* Tile Grid Background */}
<TileGrid />
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full pointer-events-none">
<div className="flex items-center justify-center min-h-[calc(100vh-12rem)] pb-20">
{/* Center Content */}
<div className="text-center relative z-10">
<div className="inline-flex items-center gap-2 bg-lime-400 text-black px-5 py-2.5 text-xs font-bold tracking-widest rounded-full mb-10 pointer-events-auto">
<span className="w-2 h-2 bg-black rounded-full animate-pulse" />
FEBRUARY 2026
</div>
<h1
ref={headingRef}
className="text-6xl sm:text-7xl lg:text-8xl xl:text-9xl font-black tracking-tighter text-white leading-[0.85] mb-8"
style={{
WebkitTextStroke: "2px black",
paintOrder: "stroke fill",
}}
>
<span className="block">UNLEASH</span>
<span
className="block text-lime-400 drop-shadow-[0_0_30px_rgba(163,230,53,0.5)]"
style={{
WebkitTextStroke: "2px black",
paintOrder: "stroke fill",
}}
>
YOUR ART
</span>
</h1>
<div ref={contentRef}>
<p className="text-zinc-300 text-base sm:text-lg max-w-lg mx-auto mb-10 leading-relaxed font-light drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)]">
From canvas to screen, showcase your creativity and let your
artwork speak to the world.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center pointer-events-auto mb-12">
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-10 py-4 text-sm font-bold tracking-wider hover:bg-lime-300 transition-all hover:scale-105 rounded-full shadow-[0_0_30px_rgba(163,230,53,0.3)]"
>
JOIN COMPETITION
</Link>
<Link
href="/gallery"
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-10 py-4 text-sm font-bold tracking-wider hover:bg-white/10 transition-all rounded-full border border-zinc-700 hover:border-zinc-500"
>
EXPLORE GALLERY
</Link>
</div>
{/* Scroll Indicator */}
<div className="flex flex-col items-center gap-2">
<span className="text-lime-400 text-[10px] tracking-[0.3em] uppercase drop-shadow-[0_0_10px_rgba(163,230,53,0.8)]">Scroll</span>
<div className="w-px h-8 bg-gradient-to-b from-lime-400 to-transparent shadow-[0_0_10px_rgba(163,230,53,0.5)]" />
</div>
</div>
</div>
</div>
</div>
{/* Bottom Marquee */}
<div className="absolute bottom-0 left-0 right-0 bg-lime-400 text-black py-3 overflow-hidden z-10">
<div className="animate-marquee-slow whitespace-nowrap flex items-center">
{Array(8)
.fill(null)
.map((_, i) => (
<div key={i} className="flex items-center">
<span className="text-xl sm:text-2xl font-black tracking-tight mx-6 uppercase">
ArtSplash
</span>
<span className="text-black/40 mx-4"></span>
<span className="text-xl sm:text-2xl font-black tracking-tight mx-6 uppercase text-black/60">
SJEC
</span>
<span className="text-black/40 mx-4"></span>
<span className="text-xl sm:text-2xl font-black tracking-tight mx-6 uppercase">
2026
</span>
<span className="text-black/40 mx-4"></span>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,101 @@
"use client";
import { motion } from "framer-motion";
import { UserPlus, Upload, Shield, Vote, Trophy } from "lucide-react";
const steps = [
{
icon: UserPlus,
step: "01",
title: "Register",
description: "Sign up with your SJEC email to participate in the competition.",
},
{
icon: Upload,
step: "02",
title: "Submit",
description: "Upload your artwork with title, description, and category details.",
},
{
icon: Shield,
step: "03",
title: "Review",
description: "Moderators review submissions to ensure guidelines are met.",
},
{
icon: Vote,
step: "04",
title: "Public Voting",
description: "The public votes for their favorite artworks in each category.",
},
{
icon: Trophy,
step: "05",
title: "Results",
description: "Winners are announced and celebrated across all categories.",
},
];
export default function HowItWorks() {
return (
<section className="py-24 bg-black text-white overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-4">
HOW IT WORKS
</span>
<h2 className="text-4xl sm:text-5xl font-black tracking-tight mb-4">
Your Journey to
<span className="text-lime-400"> Victory</span>
</h2>
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
From registration to winning, here's how ArtSplash works.
</p>
</motion.div>
{/* Steps Timeline */}
<div className="relative">
{/* Connection Line */}
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-zinc-800 hidden lg:block" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-8 lg:gap-4">
{steps.map((step, index) => (
<motion.div
key={step.step}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="relative"
>
{/* Card */}
<div className="bg-zinc-900 rounded-2xl p-6 h-full border border-zinc-800 hover:border-lime-400/50 transition-colors group">
{/* Step Number */}
<div className="relative z-10 w-12 h-12 rounded-full bg-lime-400 text-black font-black text-sm flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
{step.step}
</div>
{/* Icon */}
<step.icon className="w-8 h-8 text-lime-400 mb-4" />
{/* Content */}
<h3 className="text-xl font-bold mb-2">{step.title}</h3>
<p className="text-zinc-400 text-sm leading-relaxed">
{step.description}
</p>
</div>
</motion.div>
))}
</div>
</div>
</div>
</section>
);
}

122
app/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { Menu, X, Diamond } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
const navLinks = [
{ href: "/gallery", label: "GALLERY" },
{ href: "/submit", label: "SUBMIT ART" },
{ href: "/about", label: "ABOUT" },
{ href: "/rules", label: "RULES" },
{ href: "/faq", label: "FAQ" },
{ href: "/contact", label: "CONTACT" },
];
export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800">
{/* Announcement Bar */}
<div className="bg-lime-400 text-black overflow-hidden">
<div className="whitespace-nowrap py-2 text-xs tracking-widest flex">
{/* Track */}
<div className="animate-marquee flex">
{[...Array(10)].map((_, i) => (
<span key={`first-${i}`} className="mx-8 shrink-0">
OPEN FOR SUBMISSIONS
</span>
))}
{/* Duplicate for seamless loop */}
{[...Array(10)].map((_, i) => (
<span key={`second-${i}`} className="mx-8 shrink-0">
OPEN FOR SUBMISSIONS
</span>
))}
</div>
</div>
</div>
{/* Main Navbar */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 group">
<Diamond className="w-6 h-6 text-lime-400 group-hover:rotate-12 transition-transform" />
<span className="text-xl font-bold tracking-tight text-white">ARTSPLASH</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden lg:flex items-center gap-8">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-sm font-medium tracking-wide text-zinc-300 hover:text-white transition-colors relative group"
>
{link.label}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-lime-400 group-hover:w-full transition-all duration-300" />
</Link>
))}
</div>
{/* CTA Button */}
<div className="hidden lg:block">
<Link
href="/register"
className="bg-lime-400 text-black px-6 py-2.5 text-sm font-bold tracking-wide hover:bg-lime-300 transition-colors rounded-full"
>
REGISTER NOW
</Link>
</div>
{/* Mobile Menu Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="lg:hidden p-2 hover:bg-zinc-800 rounded-lg transition-colors text-white"
aria-label="Toggle menu"
>
{isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
{/* Mobile Menu */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="lg:hidden bg-zinc-950 border-t border-zinc-800"
>
<div className="px-4 py-6 space-y-4">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setIsOpen(false)}
className="block text-lg font-medium text-zinc-300 hover:text-white hover:pl-2 transition-all"
>
{link.label}
</Link>
))}
<Link
href="/register"
onClick={() => setIsOpen(false)}
className="block w-full text-center bg-lime-400 text-black px-6 py-3 text-sm font-bold tracking-wide hover:bg-lime-300 transition-colors rounded-full mt-6"
>
REGISTER NOW
</Link>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
);
}

38
app/components/Stats.tsx Normal file
View File

@@ -0,0 +1,38 @@
"use client";
import { motion } from "framer-motion";
const stats = [
{ value: "3", label: "Categories" },
{ value: "∞", label: "Possibilities" },
{ value: "1", label: "Winner Per Category" },
{ value: "100%", label: "Free Entry" },
];
export default function Stats() {
return (
<section className="py-16 bg-lime-400">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{stats.map((stat, index) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="text-center"
>
<div className="text-5xl sm:text-6xl font-black text-black mb-2">
{stat.value}
</div>
<div className="text-black/70 font-medium tracking-wide uppercase text-sm">
{stat.label}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,26 +1,94 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #ffffff; --background: #000000;
--foreground: #171717; --foreground: #ffffff;
--lime: #c5ff00;
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-lime-400: #c5ff00;
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@media (prefers-color-scheme: dark) { html,
:root { body {
--background: #0a0a0a; height: 100%;
--foreground: #ededed; min-height: 100vh;
}
} }
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
}
/* Custom Animations */
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.animate-marquee {
animation: marquee 60s linear infinite;
width: max-content;
}
.animate-marquee:hover {
animation-play-state: paused;
}
.animate-marquee-slow {
animation: marquee 30s linear infinite;
width: max-content;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #18181b;
}
::-webkit-scrollbar-thumb {
background: #c5ff00;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8e000;
}
/* Selection color */
::selection {
background-color: #c5ff00;
color: #000000;
}
/* Focus styles */
*:focus-visible {
outline: 2px solid #c5ff00;
outline-offset: 2px;
}
/* Gradient text utility */
.gradient-text {
background: linear-gradient(135deg, #c5ff00 0%, #84cc16 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }

View File

@@ -13,8 +13,10 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "ArtSplash | SJEC Art Competition 2026",
description: "Generated by create next app", description:
"Join SJEC's premier art competition featuring Digital Art, Paintings, and Poster Making. Register now and showcase your creativity!",
keywords: ["art competition", "SJEC", "digital art", "paintings", "poster making", "ArtSplash"],
}; };
export default function RootLayout({ export default function RootLayout({
@@ -24,9 +26,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body <body className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-black`}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children} {children}
</body> </body>
</html> </html>

View File

@@ -1,65 +1,23 @@
import Image from "next/image"; import Navbar from "./components/Navbar";
import Hero from "./components/Hero";
import Categories from "./components/Categories";
import HowItWorks from "./components/HowItWorks";
import Stats from "./components/Stats";
import CTA from "./components/CTA";
import Footer from "./components/Footer";
export default function Home() { export default function Home() {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <>
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <Navbar />
<Image <main>
className="dark:invert" <Hero />
src="/next.svg" <Categories />
alt="Next.js logo" <Stats />
width={100} <HowItWorks />
height={20} <CTA />
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main> </main>
</div> <Footer />
</>
); );
} }

View File

@@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
}; };
export default nextConfig; export default nextConfig;

2910
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,9 @@
"check": "biome check ." "check": "biome check ."
}, },
"dependencies": { "dependencies": {
"framer-motion": "^12.34.3",
"gsap": "^3.14.2",
"lucide-react": "^0.575.0",
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"

86
pnpm-lock.yaml generated
View File

@@ -8,6 +8,15 @@ importers:
.: .:
dependencies: dependencies:
framer-motion:
specifier: ^12.34.3
version: 12.34.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
gsap:
specifier: ^3.14.2
version: 3.14.2
lucide-react:
specifier: ^0.575.0
version: 0.575.0(react@19.2.3)
next: next:
specifier: 16.1.6 specifier: 16.1.6
version: 16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -79,105 +88,89 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4': '@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4': '@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4': '@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4': '@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4': '@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4': '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4': '@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5': '@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5': '@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5': '@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5': '@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5': '@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5': '@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5': '@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5': '@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5': '@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -238,28 +231,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.6': '@next/swc-linux-arm64-musl@16.1.6':
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.1.6': '@next/swc-linux-x64-gnu@16.1.6':
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.1.6': '@next/swc-linux-x64-musl@16.1.6':
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.6': '@next/swc-win32-arm64-msvc@16.1.6':
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
@@ -314,28 +303,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18': '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18': '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18': '@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18': '@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -534,6 +519,20 @@ packages:
resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==}
engines: {node: '>= 0.12'} engines: {node: '>= 0.12'}
framer-motion@12.34.3:
resolution: {integrity: sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs-extra@0.26.7: fs-extra@0.26.7:
resolution: {integrity: sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==} resolution: {integrity: sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==}
@@ -554,6 +553,9 @@ packages:
graceful-fs@4.2.11: graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
gsap@3.14.2:
resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==}
har-schema@2.0.0: har-schema@2.0.0:
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -658,28 +660,24 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2: lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -703,6 +701,11 @@ packages:
lodash@4.17.23: lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
lucide-react@0.575.0:
resolution: {integrity: sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -717,6 +720,12 @@ packages:
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
motion-dom@12.34.3:
resolution: {integrity: sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==}
motion-utils@12.29.2:
resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
mute-stream@0.0.5: mute-stream@0.0.5:
resolution: {integrity: sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==} resolution: {integrity: sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==}
@@ -1331,6 +1340,15 @@ snapshots:
combined-stream: 1.0.8 combined-stream: 1.0.8
mime-types: 2.1.35 mime-types: 2.1.35
framer-motion@12.34.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
motion-dom: 12.34.3
motion-utils: 12.29.2
tslib: 2.8.1
optionalDependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
fs-extra@0.26.7: fs-extra@0.26.7:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -1363,6 +1381,8 @@ snapshots:
graceful-fs@4.2.11: {} graceful-fs@4.2.11: {}
gsap@3.14.2: {}
har-schema@2.0.0: {} har-schema@2.0.0: {}
har-validator@5.1.5: har-validator@5.1.5:
@@ -1498,6 +1518,10 @@ snapshots:
lodash@4.17.23: {} lodash@4.17.23: {}
lucide-react@0.575.0(react@19.2.3):
dependencies:
react: 19.2.3
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -1512,6 +1536,12 @@ snapshots:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
motion-dom@12.34.3:
dependencies:
motion-utils: 12.29.2
motion-utils@12.29.2: {}
mute-stream@0.0.5: {} mute-stream@0.0.5: {}
mz@2.7.0: mz@2.7.0: