This commit is contained in:
Shoaib1M
2025-10-30 22:20:49 +05:30
7 changed files with 746 additions and 45 deletions

BIN
public/trophy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

View File

@@ -0,0 +1,214 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { Fondamento, Poppins } from "next/font/google";
import { FaGithub, FaLinkedin } from "react-icons/fa";
const orbitron = Fondamento({
subsets: ["latin"],
weight: ["400"],
});
const poppins = Poppins({
subsets: ["latin"],
weight: ["300", "400", "600"],
});
const localAchievements = [
{
title: "Google Summer of Code (GSoC)",
img: "/trophy.png",
members: [
{ name: "Bibhu", linkedin: "https://linkedin.com/in/ml4e" },
],
github: "https://github.com/ml4e",
},
{
title: "Google Summer of Code (GSoC) - Phase 2",
img: "/trophy.png",
members: [
{ name: "Sonali Lipsa Patra", linkedin: "https://linkedin.com/in/ml4e" },
],
github: "https://github.com/ml4e",
},
];
const marqueeItems = [
"Google Summer of Code",
"HackFest — AI Hackathon",
"Flipkart GRiD — Innovation Challenge",
"Research Contributions",
];
function InfiniteCarousel() {
const content = marqueeItems.concat(marqueeItems, marqueeItems);
return (
<div className="overflow-hidden w-full bg-gradient-to-r from-blue-900/70 to-black py-3 border-t border-b border-blue-700 mt-8">
<div className="flex animate-marquee whitespace-nowrap text-white text-lg md:text-xl font-semibold gap-12">
{content.map((item, i) => (
<span key={i} className="text-white/90 hover:text-blue-300 transition">
{item}
</span>
))}
</div>
<style jsx>{`
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-marquee {
animation: marquee 10s linear infinite;
}
`}</style>
</div>
);
}
export default function AchievementsPage() {
const [achievements, setAchievements] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchAchievements() {
try {
const res = await fetch("/api/achievements");
if (!res.ok) throw new Error("Failed to fetch achievements");
const data = await res.json();
setAchievements(data);
} catch (error) {
console.error("Error fetching achievements:", error);
} finally {
setLoading(false);
}
}
fetchAchievements();
}, []);
const combinedAchievements = [...localAchievements, ...achievements];
return (
<main
className={`relative min-h-screen flex flex-col items-center justify-start bg-gradient-to-b from-black via-blue-950 to-black text-white ${orbitron.className}`}
>
<div className="fixed bottom-4 left-4 w-50 md:w-78 opacity-90 z-10 animate-trophy">
<Image
src="/trophy.png"
alt="Trophy"
width={300}
height={300}
className="object-contain drop-shadow-[0_0_25px_rgba(0,120,255,0.8)]"
/>
</div>
<style jsx>{`
@keyframes trophyScale {
0% {
transform: scale(1);
filter: drop-shadow(0 0 10px rgba(0, 120, 255, 0.5));
}
50% {
transform: scale(0.9);
filter: drop-shadow(0 0 25px rgba(0, 180, 255, 0.8));
}
100% {
transform: scale(1);
filter: drop-shadow(0 0 10px rgba(0, 120, 255, 0.5));
}
}
.animate-trophy {
animation: trophyScale 4s ease-in-out infinite;
}
`}</style>
<h1
className={`text-4xl md:text-6xl font-bold mt-24 text-center tracking-widest relative ${orbitron.className}`}
>
<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)]">
ACHIEVEMENTS
</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>
<InfiniteCarousel />
<section
className={`mt-10 mb-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 px-4 md:px-12 w-full max-w-6xl ${poppins.className}`}
>
{loading ? (
<p className="text-gray-400 text-center col-span-full">Loading achievements...</p>
) : (
combinedAchievements.map((a, idx) => (
<div
key={idx}
className={`group relative bg-gradient-to-br from-blue-900/40 to-blue-800/10 border border-blue-700 rounded-xl overflow-hidden backdrop-blur-sm transition-transform duration-300 transform hover:scale-[1.04] hover:shadow-[0_0_25px_rgba(0,120,255,0.6)] flex flex-col ${
a.img ? "" : "justify-center"
}`}
>
{a.img && (
<div className="w-full h-48 relative overflow-hidden">
<Image
src={a.img}
alt={a.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-blue-400/10 opacity-0 group-hover:opacity-100 transition-opacity duration-700 blur-2xl"></div>
</div>
)}
<div className="p-6 flex flex-col flex-grow text-center">
<h2 className="text-2xl font-semibold mb-2 text-blue-200">
{a.title}
</h2>
{a.members && (
<div className="flex justify-center items-center mt-3 flex-wrap gap-3">
{a.members.map((m, i) => (
<a
key={i}
href={m.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-blue-400 text-white/80 transition"
>
<span className="text-xs">{m.name}</span>
<FaLinkedin size={14} />
</a>
))}
</div>
)}
{a.github && (
<a
href={a.github}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-white/80 hover:text-white mt-4"
>
<FaGithub size={18} />
<span className="text-sm">GitHub</span>
</a>
)}
</div>
</div>
))
)}
</section>
</main>
);
}

View File

@@ -0,0 +1,71 @@
import connectToAchievementsDB from "../../../lib/achievementsDB";
import Achievement from "../../../models/Achievements";
import { uploadToCloudinary } from "../../../lib/cloudinary";
export async function POST(req) {
try {
await connectToAchievementsDB(); // ✅ connect to DB
const formData = await req.formData();
const title = formData.get("title");
const github = formData.get("github");
const deployed = formData.get("deployed"); // ✅ new
const dateOfEvent = formData.get("dateOfEvent"); // ✅ new
const membersRaw = formData.get("members");
const certificate = formData.get("certificate");
// Parse members JSON safely
let members = [];
if (membersRaw) {
try {
members = JSON.parse(membersRaw);
} catch {
members = [];
}
}
// Upload certificate to Cloudinary
let imageUrl = "";
if (certificate) {
const buffer = Buffer.from(await certificate.arrayBuffer());
const uploadRes = await uploadToCloudinary(buffer, "achievements");
imageUrl = uploadRes.secure_url;
}
// Create new document
const newAchievement = new Achievement({
title,
github,
deployed, // ✅ added
dateOfEvent, // ✅ added
members,
img: imageUrl,
});
await newAchievement.save();
return new Response(
JSON.stringify({ message: "Achievement added successfully!" }),
{ status: 201 }
);
} catch (error) {
console.error("❌ Error saving achievement:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
}
export async function GET() {
try {
await connectToAchievementsDB();
const achievements = await Achievement.find({}).sort({ createdAt: -1 });
return new Response(JSON.stringify(achievements), { status: 200 });
} catch (error) {
console.error("❌ Error fetching achievements:", error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
}

View File

@@ -9,7 +9,7 @@ const ANIMATIONS = {
visible: { opacity: 1, transition: { duration: 0.3 } },
exit: { opacity: 0, transition: { duration: 0.3 } },
},
menuContainer: {
menuContainer: {
hidden: { opacity: 0 },
visible: {
opacity: 1,
@@ -31,11 +31,25 @@ export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const [showDropdownResources, setShowDropdownResources] = useState(false);
const [showDropdownProjects, setShowDropdownProjects] = useState(false);
const [showDropdownAchievements, setShowDropdownAchievements] = useState(false);
// ✅ Fixed keyboard listener for JSX
useEffect(() => {
const onKey = (e) => e.key === "Escape" && setIsOpen(false);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
const handleKeyDown = (event) => {
if (event.key === "Escape") {
setIsOpen(false);
}
};
if (typeof window !== "undefined") {
window.addEventListener("keydown", handleKeyDown);
}
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("keydown", handleKeyDown);
}
};
}, []);
useEffect(() => {
@@ -48,8 +62,11 @@ export default function Navbar() {
const mainLinks = [
"Home",
"Projects",
"Events",
"Team",
"Achievements",
"Resources",
"Contact",
];
@@ -73,7 +90,6 @@ export default function Navbar() {
</svg>
</div>
{/* Hamburger Button */}
<button
onClick={toggleMenu}
aria-label="Toggle menu"
@@ -99,7 +115,6 @@ export default function Navbar() {
</button>
</div>
{/* Overlay + Menu */}
<AnimatePresence>
{isOpen && (
<>
@@ -124,6 +139,87 @@ export default function Navbar() {
variants={ANIMATIONS.menuContainer}
>
{mainLinks.map((item) => {
// ✅ Mobile Achievements Dropdown
if (item === "Achievements") {
return (
<motion.div
key="achievements"
variants={ANIMATIONS.menuItem}
className="flex flex-col items-start w-full"
>
<motion.span
className="text-xl text-white font-semibold mb-2"
variants={ANIMATIONS.menuItem}
>
Achievements
</motion.span>
<motion.div
className="flex flex-col items-start gap-2 border-l-2 border-blue-500 pl-4"
variants={ANIMATIONS.menuItem}
>
<motion.a
href="/form"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
Upload Achievements
</motion.a>
<motion.a
href="/achievements"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
View Achievements
</motion.a>
</motion.div>
</motion.div>
);
}
// ✅ Mobile Projects Dropdown
if (item === "Projects") {
return (
<motion.div
key="projects"
variants={ANIMATIONS.menuItem}
className="flex flex-col items-start w-full"
>
<motion.span
className="text-xl text-white font-semibold mb-2"
variants={ANIMATIONS.menuItem}
>
Projects
</motion.span>
<motion.div
className="flex flex-col items-start gap-2 border-l-2 border-blue-500 pl-4"
variants={ANIMATIONS.menuItem}
>
<motion.a
href="/upload"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
Upload Projects
</motion.a>
<motion.a
href="/projects"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
View Projects
</motion.a>
</motion.div>
</motion.div>
);
}
// ✅ Mobile Resources Dropdown
if (item === "Resources") {
return (
<motion.div
@@ -163,44 +259,6 @@ export default function Navbar() {
);
}
if (item === "Projects") {
return (
<motion.div
key="projects"
variants={ANIMATIONS.menuItem}
className="flex flex-col items-start w-full"
>
<motion.span
className="text-xl text-white font-semibold mb-2"
variants={ANIMATIONS.menuItem}
>
Projects
</motion.span>
<motion.div
className="flex flex-col items-start gap-2 border-l-2 border-blue-500 pl-4"
variants={ANIMATIONS.menuItem}
>
<motion.a
href="/upload"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
Upload Projects
</motion.a>
<motion.a
href="/projects"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
View Projects
</motion.a>
</motion.div>
</motion.div>
);
}
if (item === "Team") {
return (
<motion.a
@@ -243,6 +301,7 @@ export default function Navbar() {
: "bg-white/10 backdrop-blur-lg border-r border-white/20 shadow-xl"
}`}
>
{/* Logo */}
<div className="w-12 h-12 rounded-full overflow-hidden ">
<svg
@@ -273,7 +332,6 @@ export default function Navbar() {
<div className="h-6" />
</div>
{/* Overlay + Menu */}
<AnimatePresence>
{isOpen && (
<>
@@ -306,6 +364,52 @@ export default function Navbar() {
variants={ANIMATIONS.menuContainer}
>
{mainLinks.map((item) => {
// ✅ Desktop Achievements Dropdown
if (item === "Achievements") {
return (
<div
key="achievements"
className="relative group"
onMouseEnter={() => setShowDropdownAchievements(true)}
onMouseLeave={() => setShowDropdownAchievements(false)}
>
<motion.span
className="text-2xl text-white hover:text-blue-300 transition-colors py-2 px-6 rounded-lg hover:bg-white/5 cursor-pointer select-none"
variants={ANIMATIONS.menuItem}
>
Achievements
</motion.span>
<AnimatePresence>
{showDropdownAchievements && (
<motion.div
className="absolute top-full left-0 mt-2 flex flex-col bg-white/10 border border-white/20 rounded-lg shadow-lg"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<a
href="/form"
className="px-4 py-2 text-white hover:bg-blue-500/20"
onClick={() => setIsOpen(false)}
>
Upload Achievements
</a>
<a
href="/achievements"
className="px-4 py-2 text-white hover:bg-blue-500/20"
onClick={() => setIsOpen(false)}
>
View Achievements
</a>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
// ✅ Desktop Projects Dropdown
if (item === "Projects") {
return (
<div
@@ -363,6 +467,7 @@ export default function Navbar() {
);
}
// ✅ Desktop Resources Dropdown
if (item === "Resources") {
return (
<div

273
src/app/form/page.tsx Normal file
View File

@@ -0,0 +1,273 @@
'use client';
import React, { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { FiUploadCloud, FiPlus } from "react-icons/fi";
const AchievementForm: React.FC = () => {
const router = useRouter();
const [formData, setFormData] = useState({
achievementName: "",
dateOfEvent: "",
github: "",
deployed: "",
});
const [members, setMembers] = useState([{ name: "", linkedin: "" }]);
const [certificate, setCertificate] = 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;
setCertificate(selectedFile);
if (selectedFile) {
setPreviewUrl(URL.createObjectURL(selectedFile));
}
};
const handleMemberChange = (index: number, field: "name" | "linkedin", value: string) => {
const updatedMembers = [...members];
updatedMembers[index][field] = value;
setMembers(updatedMembers);
};
const addMember = () => {
if (members.length < 6) {
setMembers([...members, { name: "", linkedin: "" }]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setMessage("");
try {
const form = new FormData();
form.append("achievementName", formData.achievementName);
form.append("dateOfEvent", formData.dateOfEvent);
form.append("githubLink", formData.github);
form.append("deployedLink", formData.deployed);
form.append("members", JSON.stringify(members));
if (certificate) form.append("certificate", certificate);
const res = await fetch("/api/achievements", {
method: "POST",
body: form,
});
if (res.ok) {
setMessage("Achievement submitted successfully!");
setShowModal(true);
setFormData({
achievementName: "",
dateOfEvent: "",
github: "",
deployed: "",
});
setMembers([{ name: "", linkedin: "" }]);
setCertificate(null);
setPreviewUrl(null);
setTimeout(() => {
setShowModal(false);
router.push("/projects");
}, 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/v1761625676/Screenshot_2025-10-28_095648_frjezi.png"
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 Your Achievement</h2>
<div className="mb-6">
<label htmlFor="achievementName" className="block mb-2 text-lg font-semibold">
Name of Achievement
</label>
<input
id="achievementName"
type="text"
required
value={formData.achievementName}
onChange={handleChange}
placeholder="e.g. Hackathon Winner, Best Innovation Award"
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="dateOfEvent" className="block mb-2 text-lg font-semibold">
Date of Event
</label>
<input
id="dateOfEvent"
type="date"
required
value={formData.dateOfEvent}
onChange={handleChange}
className="w-full p-3 rounded bg-transparent border border-white text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
</div>
<div className="mb-6">
<label htmlFor="github" className="block mb-2 text-lg font-semibold">
Project GitHub Link (optional)
</label>
<input
id="github"
type="url"
value={formData.github}
onChange={handleChange}
placeholder="https://github.com/yourproject"
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 className="block mb-3 text-lg font-semibold">Members</label>
{members.map((member, index) => (
<div key={index} className="flex flex-col sm:flex-row gap-3 mb-3">
<input
type="text"
placeholder={`Member ${index + 1} Name`}
value={member.name}
onChange={(e) => handleMemberChange(index, "name", e.target.value)}
className="flex-1 p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
<input
type="url"
placeholder="LinkedIn URL"
value={member.linkedin}
onChange={(e) => handleMemberChange(index, "linkedin", e.target.value)}
className="flex-1 p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
</div>
))}
{members.length < 6 && (
<button
type="button"
onClick={addMember}
className="flex items-center gap-2 px-4 py-2 border border-white rounded-lg hover:bg-blue-500 transition"
>
<FiPlus /> Add Member
</button>
)}
</div>
<div className="mb-8">
<label htmlFor="certificate" className="block mb-2 text-lg font-semibold">
Upload Certificate
</label>
<div className="flex items-center gap-3">
<label
htmlFor="certificate"
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">
{certificate ? certificate.name : "No file chosen"}
</span>
</div>
<input
id="certificate"
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="Certificate Preview"
fill
style={{ 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 Achievement"}
</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">Achievement Submitted!</h2>
<p className="text-gray-200 mb-6">
Redirecting you to the projects 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 AchievementForm;

17
src/lib/achievementsDB.js Normal file
View File

@@ -0,0 +1,17 @@
import mongoose from "mongoose";
const connectToAchievementsDB = async () => {
if (mongoose.connection.readyState >= 1) return;
try {
await mongoose.connect(process.env.MongoURL, {
dbName: "achievementDB",
});
console.log(" Connected to Achievements MongoDB");
} catch (error) {
console.error(" MongoDB Achievements connection error:", error);
}
};
export default connectToAchievementsDB;

View File

@@ -0,0 +1,21 @@
import mongoose from "mongoose";
const achievementSchema = new mongoose.Schema(
{
title: { type: String, required: true },
github: { type: String },
deployed: { type: String },
dateOfEvent: { type: Date },
members: [
{
name: String,
linkedin: String,
},
],
img: { type: String },
},
{ timestamps: true }
);
export default mongoose.models.Achievement ||
mongoose.model("Achievement", achievementSchema);