chore:added events frontend and backend
This commit is contained in:
55
src/app/api/events/route.ts
Normal file
55
src/app/api/events/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import eventDB from "@/models/events";
|
||||
import cloudinary, { uploadToCloudinary } from "../../../lib/cloudinary";
|
||||
import { NextRequest } from "next/server";
|
||||
import connectToEventDB from "@/lib/event";
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await connectToEventDB();
|
||||
|
||||
const formData = await req.formData();
|
||||
|
||||
const name = formData.get("name");
|
||||
const description = formData.get("description");
|
||||
const date = formData.get("date");
|
||||
const image = formData.get("image") as File;
|
||||
|
||||
let imageUrl = "";
|
||||
if (image) {
|
||||
const buffer = Buffer.from(await image.arrayBuffer());
|
||||
const uploadRes = await uploadToCloudinary(buffer, "projects");
|
||||
imageUrl = uploadRes.secure_url;
|
||||
}
|
||||
|
||||
const newEvent = new eventDB({
|
||||
name:name,
|
||||
description:description,
|
||||
date:date,
|
||||
image: imageUrl,
|
||||
});
|
||||
|
||||
await newEvent.save();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ message: "Event added successfully!" }),
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error(" Error saving event:", error);
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await connectToEventDB();
|
||||
const events = await eventDB.find({}).sort({ createdAt: -1 });
|
||||
return new Response(JSON.stringify(events), { status: 200 });
|
||||
} catch (error: unknown) {
|
||||
console.error(" Error fetching events:", error);
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
138
src/app/components/Timeline.tsx
Normal file
138
src/app/components/Timeline.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import TimelineItem from './TimelineItem';
|
||||
import NeuralBackground from './NeuralBackground';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
interface TimelineData {
|
||||
id: number;
|
||||
name: string;
|
||||
date: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
// Function to parse date string and convert to Date object
|
||||
const parseDate = (dateString: string): Date => {
|
||||
const months: { [key: string]: number } = {
|
||||
january: 0, february: 1, march: 2, april: 3, may: 4, june: 5,
|
||||
july: 6, august: 7, september: 8, october: 9, november: 10, december: 11
|
||||
};
|
||||
|
||||
const parts = dateString.toLowerCase().split(' ');
|
||||
const month = months[parts[0]];
|
||||
const year = parseInt(parts[1]);
|
||||
|
||||
return new Date(year, month, 1);
|
||||
};
|
||||
|
||||
export default function Timeline() {
|
||||
const [visibleItems, setVisibleItems] = useState<Set<number>>(new Set());
|
||||
const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
||||
const [timelineData, settimelineData] = useState<TimelineData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch events
|
||||
useEffect(() => {
|
||||
async function fetchEvents() {
|
||||
try {
|
||||
const response = await fetch('/api/events');
|
||||
const data = await response.json();
|
||||
settimelineData(data);
|
||||
console.log("Fetched events data:", data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
// Intersection Observer
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const id = Number(entry.target.getAttribute('data-id'));
|
||||
if (entry.isIntersecting) {
|
||||
setVisibleItems((prev) => new Set(prev).add(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.2,
|
||||
rootMargin: '0px 0px -100px 0px',
|
||||
}
|
||||
);
|
||||
|
||||
itemRefs.current.forEach((ref) => {
|
||||
if (ref) observer.observe(ref);
|
||||
});
|
||||
|
||||
return () => {
|
||||
itemRefs.current.forEach((ref) => {
|
||||
if (ref) observer.unobserve(ref);
|
||||
});
|
||||
};
|
||||
}, [timelineData]); // Re-run when timelineData changes
|
||||
|
||||
// Sort timeline data - this recalculates on every render when timelineData changes
|
||||
const sortedTimelineData = [...timelineData].sort((a, b) => {
|
||||
const dateA = parseDate(a.date);
|
||||
const dateB = parseDate(b.date);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
|
||||
// NOW you can have conditional rendering AFTER all hooks
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="relative min-h-screen py-20 px-4 sm:px-6 lg:px-8 font-[Orbitron]">
|
||||
<NeuralBackground />
|
||||
<Navbar />
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-400 mb-4"></div>
|
||||
<p className="text-cyan-300 text-xl">Loading events...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative py-20 px-4 sm:px-6 lg:px-8 font-[Orbitron]">
|
||||
<NeuralBackground />
|
||||
<Navbar />
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-32 text-center tracking-widest relative font-[Orbitron]">
|
||||
<span className="relative text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-300 to-blue-600 drop-shadow-[0_0_25px_rgba(0,200,255,0.6)]">
|
||||
OUR EVENTS
|
||||
</span>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-24 h-[3px] bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full" />
|
||||
</h1>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 w-1 h-full bg-gradient-to-b from-cyan-500/50 via-cyan-400/30 to-cyan-500/50 shadow-[0_0_20px_rgba(34,211,238,0.3)]" />
|
||||
|
||||
{sortedTimelineData.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => {
|
||||
if (el) itemRefs.current.set(index, el);
|
||||
}}
|
||||
data-id={index}
|
||||
>
|
||||
<TimelineItem
|
||||
item={item}
|
||||
isVisible={visibleItems.has(index)}
|
||||
isRight={index % 2 === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
232
src/app/components/TimelineItem.tsx
Normal file
232
src/app/components/TimelineItem.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface TimelineItemProps {
|
||||
item: {
|
||||
id: number;
|
||||
name: string;
|
||||
date: string;
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
||||
isVisible: boolean;
|
||||
isRight: boolean;
|
||||
}
|
||||
|
||||
export default function TimelineItem({ item, isVisible, isRight }: TimelineItemProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const truncateText = (text: string, maxLength: number) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
const shouldShowButton = item.description.length > 40;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative mb-32 md:mb-48">
|
||||
{/* Desktop Layout */}
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-center justify-center relative min-h-[450px]">
|
||||
{/* Content Card - With higher z-index to overlap image */}
|
||||
<div
|
||||
className={`absolute ${
|
||||
isRight ? 'left-0' : 'right-0'
|
||||
} w-[40%] top-1/2 -translate-y-1/2 ${
|
||||
isRight ? 'pr-8 text-right' : 'pl-8'
|
||||
} z-30`}
|
||||
>
|
||||
<div
|
||||
className={`transform transition-all duration-1000 ${
|
||||
isVisible
|
||||
? 'translate-x-0 opacity-100'
|
||||
: isRight
|
||||
? 'translate-x-20 opacity-0'
|
||||
: '-translate-x-20 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="bg-gradient-to-br from-slate-800/90 to-blue-950/90 backdrop-blur-md rounded-2xl p-8 border border-cyan-500/50 shadow-[0_0_50px_rgba(34,211,238,0.3)] hover:shadow-[0_0_70px_rgba(34,211,238,0.5)] transition-all duration-500 hover:scale-[1.02]">
|
||||
<h3 className="text-2xl md:text-3xl font-bold mb-3 bg-gradient-to-r from-cyan-300 to-cyan-400 bg-clip-text text-transparent drop-shadow-[0_0_30px_rgba(34,211,238,0.8)]">
|
||||
{item.name}
|
||||
</h3>
|
||||
<div className="text-cyan-300 text-sm md:text-base font-semibold mb-4 drop-shadow-[0_0_15px_rgba(34,211,238,0.6)]">
|
||||
{item.date}
|
||||
</div>
|
||||
<p className="text-cyan-200 text-sm md:text-base leading-relaxed drop-shadow-[0_0_10px_rgba(34,211,238,0.4)]">
|
||||
{shouldShowButton ? truncateText(item.description, 40) : item.description}
|
||||
</p>
|
||||
{shouldShowButton && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className={`mt-4 px-6 py-2 bg-gradient-to-r from-cyan-500 to-blue-500 text-white font-semibold rounded-lg hover:from-cyan-400 hover:to-blue-400 transition-all duration-300 shadow-[0_0_20px_rgba(34,211,238,0.5)] hover:shadow-[0_0_30px_rgba(34,211,238,0.7)] ${
|
||||
isRight ? 'ml-auto' : 'mr-auto'
|
||||
}`}
|
||||
>
|
||||
View More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center Image - Lower z-index */}
|
||||
<div className="relative z-20"> {/* Lower z-index than cards */}
|
||||
<div
|
||||
className={`transform transition-all duration-1000 delay-200 ${
|
||||
isVisible ? 'scale-100 opacity-100' : 'scale-50 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="relative w-[450px] h-[450px]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500 to-blue-500 rounded-full blur-3xl opacity-60 animate-pulse" />
|
||||
<div className="relative w-full h-full rounded-full overflow-hidden border-4 border-cyan-400/60 shadow-[0_0_60px_rgba(34,211,238,0.8)]">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -inset-4 bg-gradient-to-r from-cyan-500 via-transparent to-cyan-500 rounded-full opacity-40 blur-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Dot - Higher z-index to stay on top */}
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-6 h-6 bg-cyan-400 rounded-full shadow-[0_0_20px_rgba(34,211,238,0.8)] animate-pulse z-40" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout - Cards already properly positioned */}
|
||||
<div className="md:hidden">
|
||||
<div
|
||||
className={`transform transition-all duration-1000 delay-200 ${
|
||||
isVisible ? 'scale-100 opacity-100' : 'scale-50 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="relative w-64 h-64 mx-auto mb-8">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500 to-blue-500 rounded-full blur-2xl opacity-60 animate-pulse" />
|
||||
<div className="relative w-full h-full rounded-full overflow-hidden border-4 border-cyan-400/60 shadow-[0_0_50px_rgba(34,211,238,0.7)]">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-slate-800/90 to-blue-950/90 backdrop-blur-md rounded-2xl p-8 border border-cyan-500/50 shadow-[0_0_50px_rgba(34,211,238,0.3)]">
|
||||
<h3 className="text-2xl font-bold mb-3 bg-gradient-to-r from-cyan-300 to-cyan-400 bg-clip-text text-transparent drop-shadow-[0_0_30px_rgba(34,211,238,0.8)]">
|
||||
{item.name}
|
||||
</h3>
|
||||
<div className="text-cyan-300 text-sm font-semibold mb-4 drop-shadow-[0_0_15px_rgba(34,211,238,0.6)]">
|
||||
{item.date}
|
||||
</div>
|
||||
<p className="text-cyan-200 text-sm leading-relaxed drop-shadow-[0_0_10px_rgba(34,211,238,0.4)]">
|
||||
{shouldShowButton ? truncateText(item.description, 150) : item.description}
|
||||
</p>
|
||||
{shouldShowButton && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="mt-4 px-5 py-2 bg-gradient-to-r from-cyan-500 to-blue-500 text-white text-sm font-semibold rounded-lg hover:from-cyan-400 hover:to-blue-400 transition-all duration-300 shadow-[0_0_20px_rgba(34,211,238,0.5)]"
|
||||
>
|
||||
View More
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
{isModalOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-fadeIn"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-slate-900/80 backdrop-blur-lg" />
|
||||
|
||||
<div
|
||||
className="relative max-w-3xl w-full bg-gradient-to-br from-slate-800/95 to-blue-950/95 backdrop-blur-xl rounded-3xl p-8 md:p-12 border-2 border-cyan-500/50 shadow-[0_0_80px_rgba(34,211,238,0.4)] transform transition-all duration-300 animate-scaleIn"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="absolute top-4 right-4 w-10 h-10 flex items-center justify-center bg-cyan-500/20 hover:bg-cyan-500/40 rounded-full transition-all duration-300 group"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 text-cyan-300 group-hover:text-cyan-100"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className="md:w-1/3 flex-shrink-0">
|
||||
<div className="relative w-full aspect-square">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500 to-blue-500 rounded-2xl blur-xl opacity-60" />
|
||||
<div className="relative w-full h-full rounded-2xl overflow-hidden border-2 border-cyan-400/60 shadow-[0_0_40px_rgba(34,211,238,0.6)]">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto max-h-[60vh]">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 bg-gradient-to-r from-cyan-300 to-cyan-400 bg-clip-text text-transparent drop-shadow-[0_0_30px_rgba(34,211,238,0.8)]">
|
||||
{item.name}
|
||||
</h2>
|
||||
<div className="text-cyan-300 text-lg font-semibold mb-6 drop-shadow-[0_0_15px_rgba(34,211,238,0.6)]">
|
||||
{item.date}
|
||||
</div>
|
||||
<p className="text-cyan-200 text-base md:text-lg leading-relaxed drop-shadow-[0_0_10px_rgba(34,211,238,0.4)]">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-scaleIn {
|
||||
animation: scaleIn 0.3s ease-out;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
src/app/events/page.tsx
Normal file
9
src/app/events/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import Timeline from '@/app/components/Timeline';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-br from-slate-900 via-blue-950 to-slate-900">
|
||||
<Timeline />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
216
src/app/uploadForm/page.tsx
Normal file
216
src/app/uploadForm/page.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
import React, { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FiUploadCloud } from "react-icons/fi";
|
||||
|
||||
const UploadPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
date: "",
|
||||
});
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFormData({ ...formData, [e.target.id]: e.target.value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0] || null;
|
||||
setFile(selectedFile);
|
||||
if (selectedFile) {
|
||||
setPreviewUrl(URL.createObjectURL(selectedFile));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setMessage("");
|
||||
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("name", formData.name);
|
||||
form.append("description", formData.description);
|
||||
form.append("date", formData.date);
|
||||
if (file) form.append("image", file);
|
||||
|
||||
const res = await fetch("/api/events", {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setMessage("Event submitted successfully!");
|
||||
setShowModal(true);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
date: "",
|
||||
});
|
||||
setFile(null);
|
||||
setPreviewUrl(null);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowModal(false);
|
||||
router.push("/events");
|
||||
}, 2000);
|
||||
} else {
|
||||
setMessage("Failed to submit. Try again.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setMessage("Something went wrong.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col md:flex-row">
|
||||
<div className="hidden md:block md:w-1/2 relative">
|
||||
<Image
|
||||
src="https://res.cloudinary.com/dbnnd43kl/image/upload/v1760877041/Lucid_Origin_A_curious_cartoon_college_student_peeks_out_from__0_hsfh3l.jpg"
|
||||
alt="ml4e"
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/2 min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-b from-[#0A0A23] via-[#1B1B3A] to-[#3C4EC0]">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full max-w-xl text-white p-6 sm:p-8 rounded-xl border border-blue-500 shadow-[0_0_15px_2px_rgba(100,149,237,0.4)] backdrop-blur-sm"
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
<h2 className="text-3xl font-bold mb-8 text-center">Submit Event</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="name" className="block mb-2 text-lg font-semibold">
|
||||
Event Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Event name"
|
||||
className="w-full p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="description" className="block mb-2 text-lg font-semibold">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
required
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Brief description of the event..."
|
||||
rows={4}
|
||||
className="w-full p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="date" className="block mb-2 text-lg font-semibold">
|
||||
Event Date
|
||||
</label>
|
||||
<input
|
||||
id="date"
|
||||
type="date"
|
||||
required
|
||||
value={formData.date}
|
||||
onChange={handleChange}
|
||||
className="w-full p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<label htmlFor="image" className="block mb-2 text-lg font-semibold">
|
||||
Upload Event Image
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label
|
||||
htmlFor="image"
|
||||
className="flex items-center gap-2 px-4 py-2 border border-white rounded-lg cursor-pointer hover:bg-blue-500 transition"
|
||||
>
|
||||
<FiUploadCloud className="text-xl" />
|
||||
<span>Select File</span>
|
||||
</label>
|
||||
<span className="text-sm text-gray-300">
|
||||
{file ? file.name : "No file chosen"}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{previewUrl && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm mb-2 text-gray-300">Preview:</p>
|
||||
<div className="relative w-full h-48 rounded-lg overflow-hidden border border-blue-500">
|
||||
<Image
|
||||
src={previewUrl}
|
||||
alt="Preview"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`w-full py-3 cursor-pointer rounded font-bold transition ${
|
||||
isSubmitting
|
||||
? "bg-gray-500 cursor-not-allowed"
|
||||
: "bg-white text-[#0A0A23] hover:bg-blue-400 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting ? "Submitting..." : "Submit Event"}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<p className="text-center mt-4 text-blue-300 font-semibold">{message}</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50">
|
||||
<div className="bg-blue-600 text-white rounded-xl shadow-xl p-8 max-w-sm w-full text-center animate-fade-in">
|
||||
<h2 className="text-2xl font-bold mb-4">Event Submitted!</h2>
|
||||
<p className="text-gray-200 mb-6">
|
||||
Redirecting you to the events page...
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<div className="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadPage;
|
||||
18
src/lib/event.ts
Normal file
18
src/lib/event.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const connectToEventDB = async () => {
|
||||
|
||||
if (mongoose.connection.readyState >= 1) return;
|
||||
|
||||
try {
|
||||
await mongoose.connect(process.env.MongoURL!, {
|
||||
dbName: "eventDB",
|
||||
});
|
||||
console.log("Db name is:-", mongoose.connection.db!.databaseName);
|
||||
console.log(" Connected to Event MongoDB");
|
||||
} catch (error) {
|
||||
console.error(" MongoDB Event connection error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectToEventDB;
|
||||
14
src/models/events.ts
Normal file
14
src/models/events.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const ProjectSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
date: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
image: { type: String, required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export default mongoose.models.eventDB ||
|
||||
mongoose.model("eventDB", ProjectSchema);
|
||||
Reference in New Issue
Block a user