feat:team section, api, frontend

This commit is contained in:
Ashwani Senapati
2025-10-31 13:35:26 +05:30
parent 51843cd67c
commit 63539e9968
8 changed files with 620 additions and 106 deletions

59
src/app/api/team/route.ts Normal file
View File

@@ -0,0 +1,59 @@
import connectToTeamDB from "../../../lib/teamDB";
import TeamDB from "../../../models/Team";
import cloudinary, { uploadToCloudinary } from "../../../lib/cloudinary";
import { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
try {
await connectToTeamDB();
const formData = await req.formData();
const name = formData.get("name");
const role = formData.get("role");
// const description = formData.get("description");
const githubLink = formData.get("githubLink");
const linkedinLink = formData.get("linkedinLink");
const imageFile = formData.get("imageUrl") as File;
let imageUrl = "";
if (imageFile) {
const buffer = Buffer.from(await imageFile.arrayBuffer());
const uploadRes = await uploadToCloudinary(buffer, "projects");
imageUrl = uploadRes.secure_url;
}
const newTeamMember = new TeamDB({
name,
role,
githubLink,
linkedinLink ,
imageUrl,
});
await newTeamMember.save();
return new Response(
JSON.stringify({ message: "Team member added successfully!" }),
{ status: 201 }
);
} catch (error: unknown) {
console.error(" Error saving team member:", error);
return new Response(JSON.stringify({ error: (error as Error).message }), {
status: 500,
});
}
}
export async function GET() {
try {
await connectToTeamDB();
const teamMembers = await TeamDB.find({}).sort({ createdAt: -1 });
return new Response(JSON.stringify(teamMembers), { status: 200 });
} catch (error: unknown) {
console.error(" Error fetching team members:", error);
return new Response(JSON.stringify({ error: (error as Error).message }), {
status: 500,
});
}
}

View File

@@ -0,0 +1,63 @@
import React from 'react';
interface DefaultProfileProps {
src?: string;
alt?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}
const DefaultProfile: React.FC<DefaultProfileProps> = ({
src,
alt = 'Profile',
size = 'md',
className = ''
}) => {
const sizeClasses = {
sm: 'w-10 h-10',
md: 'w-16 h-16',
lg: 'w-24 h-24',
xl: 'w-32 h-32'
};
const iconSizes = {
sm: 'w-5 h-5',
md: 'w-8 h-8',
lg: 'w-12 h-12',
xl: 'w-16 h-16'
};
return (
<div className={`${sizeClasses[size]} ${className} relative flex items-center justify-center rounded-full overflow-hidden bg-gray-200`}>
{src ? (
<img
src={src}
alt={alt}
className="w-full h-full object-cover"
/>
) : (
// WhatsApp-style default profile icon
<svg
viewBox="0 0 212 212"
className={iconSizes[size]}
fill="none"
>
<path
d="M106.251 0.5C164.653 0.5 212 47.846 212 106.25C212 164.654 164.653 212 106.25 212C47.846 212 0.5 164.654 0.5 106.25C0.5 47.846 47.846 0.5 106.251 0.5Z"
fill="#DFE5E7"
/>
<path
d="M173.561 171.615C168.726 168.887 162.962 167.194 156.851 167.194H55.15C49.038 167.194 43.274 168.887 38.439 171.615C45.533 181.819 54.428 190.476 64.709 197.191C74.99 203.906 86.454 208.555 98.635 210.877C110.816 213.199 123.474 213.158 135.639 210.756C147.804 208.354 159.234 203.634 169.461 196.864C179.688 190.094 188.518 181.396 195.562 171.158L173.561 171.615Z"
fill="#B0BEC5"
/>
<path
d="M106.002 125.5C129.661 125.5 148.75 106.411 148.75 82.751C148.75 59.091 129.661 40.002 106.002 40.002C82.342 40.002 63.253 59.091 63.253 82.751C63.253 106.411 82.342 125.5 106.002 125.5Z"
fill="#B0BEC5"
/>
</svg>
)}
</div>
);
};
export default DefaultProfile;

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { div } from "framer-motion/client";
const ANIMATIONS = {
overlay: {
@@ -32,6 +33,7 @@ export default function Navbar() {
const [showDropdownResources, setShowDropdownResources] = useState(false);
const [showDropdownProjects, setShowDropdownProjects] = useState(false);
const [showDropdownAchievements, setShowDropdownAchievements] = useState(false);
const [showDropDownTeams, setshowDropDownTeams] = useState(false);
// ✅ Fixed keyboard listener for JSX
useEffect(() => {
@@ -218,6 +220,44 @@ export default function Navbar() {
</motion.div>
);
}
if (item === "Team") {
return (
<motion.div
key="team"
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}
>
Meet Our Team
</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="/team"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
Excecutive Body
</motion.a>
<motion.a
href="/teams"
className="text-white text-base hover:text-blue-300"
variants={ANIMATIONS.menuItem}
onClick={() => setIsOpen(false)}
>
Team Members
</motion.a>
</motion.div>
</motion.div>
);
}
// ✅ Mobile Resources Dropdown
if (item === "Resources") {
@@ -370,8 +410,8 @@ export default function Navbar() {
<div
key="achievements"
className="relative group"
onMouseEnter={() => setShowDropdownAchievements(true)}
onMouseLeave={() => setShowDropdownAchievements(false)}
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"
@@ -453,17 +493,47 @@ export default function Navbar() {
</div>
);
}
if (item === "Team") {
if (item === "Team") {
return (
<motion.a
<div
key="team"
href="/team"
variants={ANIMATIONS.menuItem}
className="text-2xl text-white hover:text-blue-300 transition-colors py-2 px-6 rounded-lg hover:bg-white/5"
onClick={() => setIsOpen(false)}
className="relative group"
onMouseEnter={() => setshowDropDownTeams(true)}
onMouseLeave={() => setshowDropDownTeams(false)}
>
Team
</motion.a>
<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}
>
Meet Our Team
</motion.span>
<AnimatePresence>
{showDropDownTeams && (
<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="/team"
className="px-4 py-2 text-white hover:bg-blue-500/20"
onClick={() => setIsOpen(false)}
>
Executive Body
</a>
<a
href="/teams"
className="px-4 py-2 text-white hover:bg-blue-500/20"
onClick={() => setIsOpen(false)}
>
Team Members
</a>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -1,6 +1,14 @@
import { motion } from "framer-motion";
import { useState } from "react";
import { Mail, Linkedin, Sparkles } from "lucide-react";
import { Mail, Linkedin, Sparkles, GithubIcon } from "lucide-react";
import DefaultProfile from "./DefaultProfile";
interface DefaultProfileProps {
src?: string;
alt?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string;
}
interface TeamMember {
name: string;
@@ -32,7 +40,7 @@ function TeamCard({ member }: TeamCardProps) {
return (
<motion.div
className="relative h-auto md:h-[440px] w-full group"
className="relative h-auto md:h-[400px] w-full group"
onHoverStart={() => !isMobile && setIsHovered(true)}
onHoverEnd={() => !isMobile && setIsHovered(false)}
onTap={() => isMobile && setIsTouched((prev) => !prev)}
@@ -214,9 +222,10 @@ function TeamCard({ member }: TeamCardProps) {
}}
/>
{/* Avatar Image */}
{/* Avatar Image */}
<div className="h-36 w-36 rounded-full ring-4 ring-white/20 relative z-10 transition-all duration-300 overflow-hidden bg-gradient-to-br from-gray-700 to-gray-900">
{!imageError ? (
{!imageError && member.avatar ? (
<img
src={member.avatar}
alt={member.name}
@@ -281,8 +290,8 @@ function TeamCard({ member }: TeamCardProps) {
} : {}}
transition={{ duration: 2, repeat: Infinity }}
>
<Mail size={18} className="text-white" />
<span className="text-white text-sm font-medium">Email</span>
<GithubIcon size={18} className="text-white" />
<span className="text-white text-sm font-medium">Github</span>
</motion.a>
{/* LinkedIn button */}
@@ -312,7 +321,7 @@ function TeamCard({ member }: TeamCardProps) {
</motion.div>
{/* Email text below - visible on hover/touch */}
<motion.p
{/* <motion.p
className={`text-xs mt-4 text-center transition-all duration-300 ${
shouldAnimate ? "text-blue-300 opacity-100" : "text-gray-500 opacity-70"
}`}
@@ -327,99 +336,11 @@ function TeamCard({ member }: TeamCardProps) {
}}
>
{member.email}
</motion.p>
</motion.p> */}
</div>
</div>
</motion.div>
);
}
export default TeamCard;
// Demo component
export default function TeamCardDemo() {
const teamMembers: TeamMember[] = [
{
name: "Sarah Johnson",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah",
email: "sarah.johnson@company.com",
linkedin: "https://linkedin.com/in/sarahjohnson"
},
{
name: "Michael Chen",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Michael",
email: "michael.chen@company.com",
linkedin: "https://linkedin.com/in/michaelchen"
},
{
name: "Emma Williams",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Emma",
email: "emma.williams@company.com",
linkedin: "https://linkedin.com/in/emmawilliams"
},
{
name: "David Martinez",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=David",
email: "david.martinez@company.com",
linkedin: "https://linkedin.com/in/davidmartinez"
},
{
name: "Lisa Anderson",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Lisa",
email: "lisa.anderson@company.com",
linkedin: "https://linkedin.com/in/lisaanderson"
},
{
name: "James Wilson",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=James",
email: "james.wilson@company.com",
linkedin: "https://linkedin.com/in/jameswilson"
},
{
name: "Sophia Brown",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Sophia",
email: "sophia.brown@company.com",
linkedin: "https://linkedin.com/in/sophiabrown"
},
{
name: "Ryan Taylor",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Ryan",
email: "ryan.taylor@company.com",
linkedin: "https://linkedin.com/in/ryantaylor"
}
];
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-black p-8">
<div className="max-w-7xl mx-auto">
<motion.h1
className="text-5xl font-bold text-white text-center mb-4"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
Our Team
</motion.h1>
<motion.p
className="text-gray-400 text-center mb-12 text-lg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
Meet the amazing people behind our success
</motion.p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{teamMembers.map((member, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
<TeamCard member={member} />
</motion.div>
))}
</div>
</div>
</div>
);
}

120
src/app/teams/page.tsx Normal file
View File

@@ -0,0 +1,120 @@
"use client";
import { motion } from "framer-motion";
import TeamCard from "@/app/components/TeamCard"; // Adjust path as needed
import { useEffect, useState } from "react";
interface TeamMember {
_id: string;
name: string;
role: string;
imageUrl: string;
githubLink: string;
linkedinLink: string;
}
export default function TeamPage() {
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const getTeamMembers = async () => {
try {
setIsLoading(true);
const res = await fetch('/api/team', {
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw new Error('Failed to fetch team members');
}
const data = await res.json();
setTeamMembers(data);
} catch (error) {
console.error('Error fetching team members:', error);
setTeamMembers([]);
} finally {
setIsLoading(false);
}
};
getTeamMembers();
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-black py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Header Section */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="text-5xl md:text-6xl font-bold text-white mb-4">
Meet Our{" "}
<span className="bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent">
Team
</span>
</h1>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Talented individuals working together to create amazing experiences
</p>
</motion.div>
{/* Team Grid */}
{isLoading ? (
<div className="text-center py-20">
<div className="inline-block p-8 bg-gray-800/50 rounded-2xl border border-gray-700/50">
<p className="text-gray-400 text-lg">Loading team members...</p>
</div>
</div>
) : teamMembers.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 md:gap-8">
{teamMembers.map((member, index) => (
<motion.div
key={member._id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: index * 0.1,
ease: "easeOut"
}}
>
<TeamCard
member={{
name: member.name,
avatar: member.imageUrl,
email: member.githubLink, // Using githubLink as email placeholder
linkedin: member.linkedinLink,
}}
/>
</motion.div>
))}
</div>
) : (
<motion.div
className="text-center py-20"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<div className="inline-block p-8 bg-gray-800/50 rounded-2xl border border-gray-700/50">
<p className="text-gray-400 text-lg mb-2">No team members found</p>
<p className="text-gray-500 text-sm">Add team members to see them here</p>
</div>
</motion.div>
)}
</div>
{/* Background Effects */}
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
</div>
</div>
);
}

249
src/app/uploadTeam/page.tsx Normal file
View File

@@ -0,0 +1,249 @@
'use client';
import React, { ReactEventHandler, 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:"",
role:"",
githubLink:"",
linkedinLink:"",
imageUrl:""
});
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState("");
const [showModal, setShowModal] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.id]: e.target.value });
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0] || null;
setFile(selectedFile);
if (selectedFile) {
setPreviewUrl(URL.createObjectURL(selectedFile));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setMessage("");
try {
const form = new FormData();
form.append("name", formData.name);
form.append("role", formData.role);
form.append("githubLink", formData.githubLink);
form.append("linkedinLink", formData.linkedinLink);
if (file) form.append("imageUrl", file);
const res = await fetch("/api/team", {
method: "POST",
body: form,
});
if (res.ok) {
setMessage("Details submitted successfully!");
setShowModal(true);
setFormData({
name:"",
role:"",
githubLink:"",
linkedinLink:"",
imageUrl:""
});
setFile(null);
setPreviewUrl(null);
setTimeout(() => {
setShowModal(false);
router.push("/teams");
}, 2000);
} else {
setMessage("Failed to submit. Try again.");
}
} catch (err) {
console.error(err);
setMessage("Something went wrong.");
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen flex flex-col md:flex-row">
<div className="hidden md:block md:w-1/2 relative">
<Image
src="https://res.cloudinary.com/dbnnd43kl/image/upload/v1760877041/Lucid_Origin_A_curious_cartoon_college_student_peeks_out_from__0_hsfh3l.jpg"
alt="ml4e"
fill
style={{ objectFit: "cover" }}
priority
/>
</div>
<div className="w-full md:w-1/2 min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-b from-[#0A0A23] via-[#1B1B3A] to-[#3C4EC0]">
<form
onSubmit={handleSubmit}
className="w-full max-w-xl text-white p-6 sm:p-8 rounded-xl border border-blue-500 shadow-[0_0_15px_2px_rgba(100,149,237,0.4)] backdrop-blur-sm"
encType="multipart/form-data"
>
<h2 className="text-3xl font-bold mb-8 text-center">Submit Your Details</h2>
<div className="mb-6">
<label htmlFor="projectName" className="block mb-2 text-lg font-semibold">
Name
</label>
<input
id="name"
type="text"
required
value={formData.name}
onChange={handleChange}
placeholder="Your Name"
className="w-full p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
</div>
<div className="mb-6">
<label htmlFor="role" className="block mb-2 text-lg font-semibold">
Role
</label>
<select
id="role"
required
value={formData.role}
onChange={handleChange as ReactEventHandler}
className="w-full p-3 rounded bg-transparent border border-white text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
>
<option value="" disabled className="bg-gray-800">
Select a role
</option>
<option value="Machine Learning" className="bg-gray-800">
Machine Learning
</option>
<option value="Web Developer" className="bg-gray-800">
Web Developer
</option>
<option value="Designer" className="bg-gray-800">
Designer
</option>
<option value="Management" className="bg-gray-800">
Management
</option>
</select>
</div>
<div className="mb-6">
<label htmlFor="github" className="block mb-2 text-lg font-semibold">
GitHub Url
</label>
<input
id="githubLink"
type="url"
value={formData.githubLink}
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-6">
<label htmlFor="deployed" className="block mb-2 text-lg font-semibold">
Linkedin Url
</label>
<input
id="linkedinLink"
type="url"
value={formData.linkedinLink}
onChange={handleChange}
placeholder=""
className="w-full p-3 rounded bg-transparent border border-white placeholder-gray-400 text-white focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
</div>
<div className="mb-8">
<label htmlFor="screenshot" className="block mb-2 text-lg font-semibold">
Your Image
</label>
<div className="flex items-center gap-3">
<label
htmlFor="screenshot"
className="flex items-center gap-2 px-4 py-2 border border-white rounded-lg cursor-pointer hover:bg-blue-500 transition"
>
<FiUploadCloud className="text-xl" />
<span>Select File</span>
</label>
<span className="text-sm text-gray-300">
{file ? file.name : "No file chosen"}
</span>
</div>
<input
id="screenshot"
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
{previewUrl && (
<div className="mt-4">
<p className="text-sm mb-2 text-gray-300">Preview:</p>
<div className="relative w-full h-48 rounded-lg overflow-hidden border border-blue-500">
<Image
src={previewUrl}
alt="Preview"
layout="fill"
objectFit="cover"
className="rounded-lg"
/>
</div>
</div>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className={`w-full py-3 cursor-pointer rounded font-bold transition ${
isSubmitting
? "bg-gray-500 cursor-not-allowed"
: "bg-white text-[#0A0A23] hover:bg-blue-400 hover:text-white"
}`}
>
{isSubmitting ? "Submitting..." : "Submit"}
</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">Details Submitted!</h2>
<p className="text-gray-200 mb-6">
Redirecting you to the teams page...
</p>
<div className="flex justify-center">
<div className="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
</div>
</div>
)}
</div>
);
};
export default UploadPage;

17
src/lib/teamDB.ts Normal file
View File

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

15
src/models/Team.ts Normal file
View File

@@ -0,0 +1,15 @@
import mongoose from "mongoose";
const ProjectSchema = new mongoose.Schema(
{
name: { type: String, required: true },
role: { type: String, required: true },
githubLink: { type: String },
linkedinLink: { type: String },
imageUrl: { type: String },
},
{ timestamps: true }
);
export default mongoose.models.TeamDB ||
mongoose.model("TeamDB", ProjectSchema);