feat:team section, api, frontend
This commit is contained in:
59
src/app/api/team/route.ts
Normal file
59
src/app/api/team/route.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
63
src/app/components/DefaultProfile.tsx
Normal file
63
src/app/components/DefaultProfile.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
120
src/app/teams/page.tsx
Normal 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
249
src/app/uploadTeam/page.tsx
Normal 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
17
src/lib/teamDB.ts
Normal 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
15
src/models/Team.ts
Normal 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);
|
||||
Reference in New Issue
Block a user