Merge branch 'main' of https://github.com/AshwaniNITR/ML4EWeb1
This commit is contained in:
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 } },
|
||||
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
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