From 1ea24e850cb9856011e47190834e6004716c98ba Mon Sep 17 00:00:00 2001 From: Ashwani Senapati Date: Mon, 3 Nov 2025 12:30:16 +0530 Subject: [PATCH] chore:added events frontend and backend --- src/app/api/events/route.ts | 55 +++++++ src/app/components/Timeline.tsx | 138 +++++++++++++++++ src/app/components/TimelineItem.tsx | 232 ++++++++++++++++++++++++++++ src/app/events/page.tsx | 9 ++ src/app/uploadForm/page.tsx | 216 ++++++++++++++++++++++++++ src/lib/event.ts | 18 +++ src/models/events.ts | 14 ++ 7 files changed, 682 insertions(+) create mode 100644 src/app/api/events/route.ts create mode 100644 src/app/components/Timeline.tsx create mode 100644 src/app/components/TimelineItem.tsx create mode 100644 src/app/events/page.tsx create mode 100644 src/app/uploadForm/page.tsx create mode 100644 src/lib/event.ts create mode 100644 src/models/events.ts diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts new file mode 100644 index 0000000..0e07250 --- /dev/null +++ b/src/app/api/events/route.ts @@ -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, + }); + } +} diff --git a/src/app/components/Timeline.tsx b/src/app/components/Timeline.tsx new file mode 100644 index 0000000..bf54079 --- /dev/null +++ b/src/app/components/Timeline.tsx @@ -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>(new Set()); + const itemRefs = useRef>(new Map()); + const [timelineData, settimelineData] = useState([]); + 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 ( +
+ + +
+
+
+

Loading events...

+
+
+
+ ); + } + + return ( +
+ + +
+

+ + OUR EVENTS + +
+

+ +
+
+ + {sortedTimelineData.map((item, index) => ( +
{ + if (el) itemRefs.current.set(index, el); + }} + data-id={index} + > + +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/components/TimelineItem.tsx b/src/app/components/TimelineItem.tsx new file mode 100644 index 0000000..e9c4c0c --- /dev/null +++ b/src/app/components/TimelineItem.tsx @@ -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 ( + <> +
+ {/* Desktop Layout */} +
+
+ {/* Content Card - With higher z-index to overlap image */} +
+
+
+

+ {item.name} +

+
+ {item.date} +
+

+ {shouldShowButton ? truncateText(item.description, 40) : item.description} +

+ {shouldShowButton && ( + + )} +
+
+
+ + {/* Center Image - Lower z-index */} +
{/* Lower z-index than cards */} +
+
+
+
+ {item.name} +
+
+
+
+ + {/* Timeline Dot - Higher z-index to stay on top */} +
+
+
+
+ + {/* Mobile Layout - Cards already properly positioned */} +
+
+
+
+
+ {item.name} +
+
+
+
+

+ {item.name} +

+
+ {item.date} +
+

+ {shouldShowButton ? truncateText(item.description, 150) : item.description} +

+ {shouldShowButton && ( + + )} +
+
+
+ + {/* Modal */} + {isModalOpen && ( +
setIsModalOpen(false)} + > +
+ +
e.stopPropagation()} + > + + +
+
+
+
+
+ {item.name} +
+
+
+ +
+

+ {item.name} +

+
+ {item.date} +
+

+ {item.description} +

+
+
+
+
+ )} + + + + ); +} \ No newline at end of file diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx new file mode 100644 index 0000000..e3cae1e --- /dev/null +++ b/src/app/events/page.tsx @@ -0,0 +1,9 @@ +import Timeline from '@/app/components/Timeline'; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/app/uploadForm/page.tsx b/src/app/uploadForm/page.tsx new file mode 100644 index 0000000..94267db --- /dev/null +++ b/src/app/uploadForm/page.tsx @@ -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(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [message, setMessage] = useState(""); + const [showModal, setShowModal] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.id]: e.target.value }); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + 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 ( +
+
+ ml4e +
+ +
+
+

Submit Event

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + {file ? file.name : "No file chosen"} + +
+ + + {previewUrl && ( +
+

Preview:

+
+ Preview +
+
+ )} +
+ + + + {message && ( +

{message}

+ )} +
+
+ + {showModal && ( +
+
+

Event Submitted!

+

+ Redirecting you to the events page... +

+
+
+
+
+
+ )} +
+ ); +}; + +export default UploadPage; \ No newline at end of file diff --git a/src/lib/event.ts b/src/lib/event.ts new file mode 100644 index 0000000..6b252cb --- /dev/null +++ b/src/lib/event.ts @@ -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; diff --git a/src/models/events.ts b/src/models/events.ts new file mode 100644 index 0000000..2422006 --- /dev/null +++ b/src/models/events.ts @@ -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); \ No newline at end of file