chore:added events frontend and backend

This commit is contained in:
Ashwani Senapati
2025-11-03 12:30:16 +05:30
parent a32e231a7f
commit 1ea24e850c
7 changed files with 682 additions and 0 deletions

View 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,
});
}
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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);