BIN
public/trophy.png
Normal file
BIN
public/trophy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
214
src/app/achievements/page.tsx
Normal file
214
src/app/achievements/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/app/api/achievements/route.js
Normal file
71
src/app/api/achievements/route.js
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ const ANIMATIONS = {
|
|||||||
visible: { opacity: 1, transition: { duration: 0.3 } },
|
visible: { opacity: 1, transition: { duration: 0.3 } },
|
||||||
exit: { opacity: 0, transition: { duration: 0.3 } },
|
exit: { opacity: 0, transition: { duration: 0.3 } },
|
||||||
},
|
},
|
||||||
menuContainer: {
|
ok menuContainer: {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@@ -31,11 +31,25 @@ export default function Navbar() {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [showDropdownResources, setShowDropdownResources] = useState(false);
|
const [showDropdownResources, setShowDropdownResources] = useState(false);
|
||||||
const [showDropdownProjects, setShowDropdownProjects] = useState(false);
|
const [showDropdownProjects, setShowDropdownProjects] = useState(false);
|
||||||
|
const [showDropdownAchievements, setShowDropdownAchievements] = useState(false);
|
||||||
|
|
||||||
|
// ✅ Fixed keyboard listener for JSX
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e) => e.key === "Escape" && setIsOpen(false);
|
const handleKeyDown = (event) => {
|
||||||
window.addEventListener("keydown", onKey);
|
if (event.key === "Escape") {
|
||||||
return () => window.removeEventListener("keydown", onKey);
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -48,8 +62,11 @@ export default function Navbar() {
|
|||||||
const mainLinks = [
|
const mainLinks = [
|
||||||
"Home",
|
"Home",
|
||||||
"Projects",
|
"Projects",
|
||||||
|
|
||||||
"Events",
|
"Events",
|
||||||
|
|
||||||
"Team",
|
"Team",
|
||||||
|
"Achievements",
|
||||||
"Resources",
|
"Resources",
|
||||||
"Contact",
|
"Contact",
|
||||||
];
|
];
|
||||||
@@ -59,6 +76,7 @@ export default function Navbar() {
|
|||||||
{/* ---------------- MOBILE NAV ---------------- */}
|
{/* ---------------- MOBILE NAV ---------------- */}
|
||||||
<nav className="md:hidden fixed top-0 left-0 w-full bg-transparent z-[60]">
|
<nav className="md:hidden fixed top-0 left-0 w-full bg-transparent z-[60]">
|
||||||
<div className="flex justify-between items-center h-16 px-4">
|
<div className="flex justify-between items-center h-16 px-4">
|
||||||
|
<div className="text-blue-500 text-2xl font-bold">ML4E</div>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="w-12 h-12 rounded-full overflow-hidden ">
|
<div className="w-12 h-12 rounded-full overflow-hidden ">
|
||||||
<svg
|
<svg
|
||||||
@@ -73,7 +91,6 @@ export default function Navbar() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hamburger Button */}
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
@@ -99,7 +116,6 @@ export default function Navbar() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay + Menu */}
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<>
|
<>
|
||||||
@@ -124,6 +140,87 @@ export default function Navbar() {
|
|||||||
variants={ANIMATIONS.menuContainer}
|
variants={ANIMATIONS.menuContainer}
|
||||||
>
|
>
|
||||||
{mainLinks.map((item) => {
|
{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") {
|
if (item === "Resources") {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -163,44 +260,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") {
|
if (item === "Team") {
|
||||||
return (
|
return (
|
||||||
<motion.a
|
<motion.a
|
||||||
@@ -243,6 +302,7 @@ export default function Navbar() {
|
|||||||
: "bg-white/10 backdrop-blur-lg border-r border-white/20 shadow-xl"
|
: "bg-white/10 backdrop-blur-lg border-r border-white/20 shadow-xl"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<div className="text-white text-2xl font-bold">ML4E</div>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="w-12 h-12 rounded-full overflow-hidden ">
|
<div className="w-12 h-12 rounded-full overflow-hidden ">
|
||||||
<svg
|
<svg
|
||||||
@@ -273,7 +333,6 @@ export default function Navbar() {
|
|||||||
<div className="h-6" />
|
<div className="h-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlay + Menu */}
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<>
|
<>
|
||||||
@@ -306,6 +365,52 @@ export default function Navbar() {
|
|||||||
variants={ANIMATIONS.menuContainer}
|
variants={ANIMATIONS.menuContainer}
|
||||||
>
|
>
|
||||||
{mainLinks.map((item) => {
|
{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") {
|
if (item === "Projects") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -363,6 +468,7 @@ export default function Navbar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Desktop Resources Dropdown
|
||||||
if (item === "Resources") {
|
if (item === "Resources") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
273
src/app/form/page.tsx
Normal file
273
src/app/form/page.tsx
Normal 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
17
src/lib/achievementsDB.js
Normal 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;
|
||||||
21
src/models/Achievements.js
Normal file
21
src/models/Achievements.js
Normal 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);
|
||||||
Reference in New Issue
Block a user