diff --git a/app/(root)/TableComponent.tsx b/app/(root)/TableComponent.tsx index 1c71aed..1714f57 100644 --- a/app/(root)/TableComponent.tsx +++ b/app/(root)/TableComponent.tsx @@ -1,31 +1,8 @@ "use client"; import { useRouter } from "next/navigation"; import React from "react"; -const data = [ - { - id: 1, - platform: "LinkedIn", - title: "Pranav, Radhika Gupta has a new post for you", - description: "To every woman who sometimes wonders, can I do it, here is a thought...", - date: "Mar 17", - }, - { - id: 2, - platform: "LinkedIn", - title: "Pranav, Nikhil Kamath has a new post for you", - description: "Digressing from the post, but it's about time we need an Indian answer to...", - date: "Mar 14", - }, - { - id: 3, - platform: "LinkedIn", - title: "Pranav, Nikhil Kamath has a new post for you", - description: "Digressing from the post, but it's about time we need an Indian answer to...", - date: "Mar 12", - }, -]; - +const data: Array<{id: number; platform: string; title: string; description: string; date: string;}> = []; const TableComponent = () => { const router = useRouter(); @@ -33,22 +10,35 @@ const TableComponent = () => { return ( - {data.map((item, index) => ( - router.push(`/notification/${item.id}`)} - > - - - + - - ))} + ) : ( + data.map((item) => ( + router.push(`/notification/${item.id}`)} + > + + + + + + )) + )}
- e.stopPropagation()} /> - {item.platform} - {item.title} - {item.description} + {data.length === 0 ? ( +
+ No items to display. {item.date}
+ e.stopPropagation()} + className="accent-brand-500" + /> + {item.platform} + {item.title} + — {item.description} + {item.date}
); diff --git a/app/(root)/TabsComponent2.tsx b/app/(root)/TabsComponent2.tsx index f0ceb7e..02d4b1c 100644 --- a/app/(root)/TabsComponent2.tsx +++ b/app/(root)/TabsComponent2.tsx @@ -1,80 +1,95 @@ -'use client'; +"use client"; -import * as React from 'react'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; -import Box from '@mui/material/Box'; +import * as React from "react"; +import Tabs from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Box from "@mui/material/Box"; interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; + children?: React.ReactNode; + index: number; + value: number; } interface TabsProps { - tabTitles: string[]; - tabContents: React.ReactNode[]; + tabTitles: string[]; + tabContents: React.ReactNode[]; } function CustomTabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); + const { children, value, index, ...other } = props; + return ( + + ); } function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - }; + return { + id: `tab-${index}`, + "aria-controls": `tabpanel-${index}`, + }; } export default function TabsComponent2({ tabTitles, tabContents }: TabsProps) { - const [value, setValue] = React.useState(0); + const [value, setValue] = React.useState(0); - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); - }; + const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; - return ( - - - - {tabTitles.map((title, index) => ( - - ))} - - - - {tabContents.map((content, index) => ( - - {content} - - ))} - - - ); + return ( + + + + {tabTitles.map((title, index) => ( + + ))} + + + + {tabContents.map((content, index) => ( + + {content} + + ))} + + + ); } diff --git a/app/(root)/application/ApplicationsList.tsx b/app/(root)/application/ApplicationsList.tsx new file mode 100644 index 0000000..dc48331 --- /dev/null +++ b/app/(root)/application/ApplicationsList.tsx @@ -0,0 +1,86 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { Clock } from "lucide-react"; +import Link from "next/link"; + +interface JoinRequest { + _id: string; + project: { _id: string; title: string }; + status: string; + createdAt: string; +} + +const ApplicationsList = ({ filterStatus }: { filterStatus: string }) => { + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchRequests = async () => { + try { + const response = await axios.get("/api/applications"); + const filtered = response.data.filter( + (r: JoinRequest) => r.status === filterStatus + ); + setRequests(filtered); + } catch (error) { + console.error("Error fetching applications:", error); + } finally { + setLoading(false); + } + }; + + fetchRequests(); + }, [filterStatus]); + + if (loading) { + return ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ); + } + + if (requests.length === 0) { + return ( +
+ No applications found. +
+ ); + } + + return ( +
+ {requests.map((item) => ( + +
+ + {item.project?.title || "Unknown Project"} + +
+ + {item.status} + + + + {new Date(item.createdAt).toLocaleDateString()} + +
+
+ + ))} +
+ ); +}; + +export default ApplicationsList; diff --git a/app/(root)/application/page.tsx b/app/(root)/application/page.tsx index b1f867f..e8de568 100644 --- a/app/(root)/application/page.tsx +++ b/app/(root)/application/page.tsx @@ -1,38 +1,60 @@ +"use client"; import React from "react"; import TabsComponent2 from "../TabsComponent2"; -import TableComponent from "../TableComponent"; -import Image from "next/image"; +import ApplicationsList from "./ApplicationsList"; +import useAuthStore from "@/app/store/useAuthStore"; +import { motion } from "framer-motion"; +import { Briefcase, Sparkles } from "lucide-react"; -// Tab contents with table components const tabContents = [ - , - , - , + , + , + , ]; const Page = () => { - const tabTitles = ["Submitted(56)", "Bookmarks(23)", "Rejected(23)"]; + const { user } = useAuthStore(); + const tabTitles = ["Pending", "Accepted", "Rejected"]; + return ( -
-
Hello, Rohit!
-
-
-

Welcome to Application Status.

-

- This page is designed to help you track and manage your project - collaboration applications with ease. View the status of your - submissions, from drafts to approved or rejected applications, all - in one place. Stay organized, keep track of your progress, and - refine your proposals for future opportunities. +

+ + Hello, {user?.firstName || "Developer"}! + + + +
+
+ +

Application Status

+
+

+ Track and manage your project collaboration applications. View submissions, + bookmarks, and stay informed about your progress.

-
- +
+
-
+
-
Applications
- + +

Applications

+ +
); }; diff --git a/app/(root)/components/AchievementsCard.tsx b/app/(root)/components/AchievementsCard.tsx new file mode 100644 index 0000000..11715ed --- /dev/null +++ b/app/(root)/components/AchievementsCard.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Award } from "lucide-react"; + +const AchievementsCard = () => { + return ( +
+
+

+ + Achievements +

+
+
+
+
+ +
+
+

First Project Completed

+

Complete your first project to earn this badge

+
+
+
+
+ +
+
+

Team Player

+

Join 5 projects to unlock

+
+
+
+
+ +
+
+

S-Rank Developer

+

Reach S rank to unlock

+
+
+
+
+ ); +}; + +export default AchievementsCard; diff --git a/app/(root)/components/Badge.tsx b/app/(root)/components/Badge.tsx new file mode 100644 index 0000000..3b2c7f4 --- /dev/null +++ b/app/(root)/components/Badge.tsx @@ -0,0 +1,29 @@ +import React, { FC } from "react"; + +type BadgeProps = { + title: string; + color: string; +}; + +const colorMap: { [key: string]: { bg: string; border: string; text: string; glow: string } } = { + red: { bg: "bg-red-500/10", border: "border-red-500/30", text: "text-red-500", glow: "shadow-[0_0_20px_rgba(239,68,68,0.3)]" }, + blue: { bg: "bg-blue-500/10", border: "border-blue-500/30", text: "text-blue-500", glow: "shadow-[0_0_20px_rgba(59,130,246,0.3)]" }, + yellow: { bg: "bg-amber-500/10", border: "border-amber-500/30", text: "text-amber-500", glow: "shadow-[0_0_20px_rgba(245,158,11,0.3)]" }, + green: { bg: "bg-green-500/10", border: "border-green-500/30", text: "text-green-500", glow: "shadow-[0_0_20px_rgba(16,185,129,0.3)]" }, + purple: { bg: "bg-purple-500/10", border: "border-purple-500/30", text: "text-purple-500", glow: "shadow-[0_0_20px_rgba(139,92,246,0.3)]" }, +}; + +const defaultColor = { bg: "bg-gray-500/10", border: "border-gray-500/30", text: "text-gray-500", glow: "" }; + +const Badge: FC = ({ title, color }) => { + const c = colorMap[color] || defaultColor; + return ( +
+
+ {title} +
+
+ ); +}; + +export default Badge; diff --git a/app/(root)/components/EditProfile.tsx b/app/(root)/components/EditProfile.tsx new file mode 100644 index 0000000..4e485da --- /dev/null +++ b/app/(root)/components/EditProfile.tsx @@ -0,0 +1,260 @@ +"use client"; +import useAuthStore from "@/app/store/useAuthStore"; +import { User } from "@/app/types/user"; +import Image from "next/image"; +import React, { useState } from "react"; +import { toast } from "sonner"; +import axios from "axios"; +import { X, Plus } from "lucide-react"; + +type EditProfileProps = { + onProfileUpdate: (data: User) => void; +}; + +const EditProfile = ({ onProfileUpdate }: EditProfileProps) => { + const { user, setUser } = useAuthStore(); + + const [firstName, setFirstName] = useState(user?.firstName || ""); + const [lastName, setLastName] = useState(user?.lastName || ""); + const [email, setEmail] = useState(user?.email || ""); + const [instituteName, setInstituteName] = useState(user?.collegeDetails?.name || ""); + const [location, setLocation] = useState(user?.location || ""); + const [github, setGithub] = useState(user?.github || ""); + const [linkedin, setLinkedin] = useState(user?.linkedin || ""); + const [skills, setSkills] = useState(user?.skills || []); + const [newSkill, setNewSkill] = useState(""); + const [isChecked, setIsChecked] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const addSkill = () => { + if (newSkill.trim() && !skills.includes(newSkill.trim())) { + setSkills([...skills, newSkill.trim()]); + setNewSkill(""); + } + }; + + const removeSkill = (index: number) => { + setSkills(skills.filter((_, i) => i !== index)); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + const toastId = toast.loading("Saving profile..."); + + const updatedProfileData: User = { + firstName, + lastName, + id: user?.id || "", + name: `${firstName} ${lastName}`, + email, + collegeDetails: { name: instituteName }, + location, + skills, + github, + linkedin, + }; + + try { + await axios.put( + `${process.env.NEXT_PUBLIC_CLIENT_URL}/api/user/${user?.id}`, + updatedProfileData + ); + + // Update Zustand store + setUser({ + ...user, + ...updatedProfileData, + id: user?.id || "", + }); + + toast.success("Profile updated successfully!", { id: toastId }); + onProfileUpdate(updatedProfileData); + } catch (error) { + console.error("Error updating profile:", error); + toast.error("Failed to update profile.", { id: toastId }); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {/* Left Panel */} + + + {/* Form */} +
+ {/* Full Name */} +
+ +
+ setFirstName(e.target.value)} + placeholder="First name*" + className="flex-1 px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> + setLastName(e.target.value)} + placeholder="Last name*" + className="flex-1 px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> +
+
+ + {/* Email */} +
+ + setEmail(e.target.value)} + placeholder="E-mail Address" + className="w-full px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> +
+ + {/* Institute */} +
+ + setInstituteName(e.target.value)} + placeholder="Institute name" + className="w-full px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> +
+ + {/* Location */} +
+ + setLocation(e.target.value)} + placeholder="City, Country" + className="w-full px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> +
+ + {/* Skills */} +
+ +
+ setNewSkill(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addSkill())} + placeholder="Enter a skill" + className="flex-1 px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> + +
+
+ {skills.length > 0 ? ( + skills.map((item, index) => ( +
+ {item} + +
+ )) + ) : ( +

No skills added yet

+ )} +
+
+ + {/* Social Links */} +
+ +
+ setGithub(e.target.value)} + placeholder="GitHub URL" + className="w-full px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> + setLinkedin(e.target.value)} + placeholder="LinkedIn URL" + className="w-full px-4 py-3 text-sm bg-theme-card border border-theme-secondary rounded-xl text-theme-primary placeholder:text-theme-tertiary focus:outline-none focus:ring-2 focus:ring-brand-500/40 focus:border-brand-500 transition-all" + /> +
+
+ + {/* Checkbox */} +
+ setIsChecked(!isChecked)} + className="w-4 h-4 rounded accent-brand-500" + /> +

+ I hereby certify that the provided information is true and accurate. +

+
+ + {/* Submit */} + +
+
+ ); +}; + +export default EditProfile; diff --git a/app/(root)/components/Hero.tsx b/app/(root)/components/Hero.tsx new file mode 100644 index 0000000..80404cc --- /dev/null +++ b/app/(root)/components/Hero.tsx @@ -0,0 +1,80 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import "./hero.css"; +import Image from "next/image"; + +interface Image { + src: string; + label: string; +} + +const images: Image[] = [ + { src: "/devs.png", label: "Adarsh" }, + { src: "/devs.png", label: "Adarsh" }, + { src: "/devs.png", label: "Adarsh" }, + { src: "/devs.png", label: "Adarsh" }, +]; + +const Hero: React.FC = () => { + const [activeImage, setActiveImage] = useState(0); + const [exiting, setExiting] = useState(false); + const [textEditing, setTextEditing] = useState(false); + + console.log("", textEditing); + + const handleButtonClick = useCallback( + (index: number): void => { + if (index !== activeImage) { + setExiting(true); + setTextEditing(true); + setTimeout(() => { + setActiveImage(index); + setExiting(false); + setTimeout(() => { + setTextEditing(false); + }, 720); + }, 500); + } + }, + [activeImage, setExiting, setTextEditing, setActiveImage] + ); + // Handle automatic image rotation every 2 seconds + useEffect(() => { + const interval = setInterval(() => { + const nextIndex = (activeImage + 1) % images.length; + handleButtonClick(nextIndex); + }, 4000); // Rotate every 2 seconds + + return () => clearInterval(interval); // Cleanup interval on component unmount + }, [activeImage, handleButtonClick]); + + return ( + <> +
+ {activeImage !== null && ( + <> +
+
+
+ {images[activeImage].label} +
+
+ + )} +
+ + ); +}; + +export default Hero; diff --git a/app/(root)/components/OnboardingModal.tsx b/app/(root)/components/OnboardingModal.tsx new file mode 100644 index 0000000..a613d19 --- /dev/null +++ b/app/(root)/components/OnboardingModal.tsx @@ -0,0 +1,152 @@ +"use client"; +import React, { useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; +import { User } from "@/app/types/user"; +import useAuthStore from "@/app/store/useAuthStore"; +import { motion } from "framer-motion"; +import { Compass, Sparkles } from "lucide-react"; + +interface OnboardingProps { + onComplete: (user: User) => void; +} + +const OnboardingModal = ({ onComplete }: OnboardingProps) => { + const { user, setUser } = useAuthStore(); + const [firstName, setFirstName] = useState(user?.name?.split(" ")[0] || ""); + const [lastName, setLastName] = useState(user?.name?.split(" ").slice(1).join(" ") || ""); + const [instituteName, setInstituteName] = useState(""); + const [location, setLocation] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!firstName || !lastName || !instituteName) { + toast.error("Please fill in all required fields."); + return; + } + + setIsSubmitting(true); + const toastId = toast.loading("Setting up your profile..."); + + const updatedProfileData = { + firstName, + lastName, + name: `${firstName} ${lastName}`, + collegeDetails: { name: instituteName }, + location, + }; + + try { + await axios.put( + `${process.env.NEXT_PUBLIC_CLIENT_URL}/api/user/${user?.id}`, + updatedProfileData + ); + + const completeUser = { + ...user, + ...updatedProfileData, + id: user?.id || "", + } as User; + + setUser(completeUser); + toast.success("Welcome to DevGuild!", { id: toastId }); + onComplete(completeUser); + } catch (error) { + console.error("Error setting up profile:", error); + toast.error("Failed to complete setup. Please try again.", { id: toastId }); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ +
+
+ +
+
+ +
+

+ Welcome to DevGuild +

+

+ Let's complete your Guild Card before you explore the platform. + This information connects you with the right teams. +

+
+ +
+
+
+ + setFirstName(e.target.value)} + className="w-full p-3 text-sm bg-theme-tertiary border border-theme-primary rounded-xl text-theme-primary focus:outline-none focus:ring-2 focus:ring-brand-500/40 transition-all" + placeholder="John" + required + /> +
+
+ + setLastName(e.target.value)} + className="w-full p-3 text-sm bg-theme-tertiary border border-theme-primary rounded-xl text-theme-primary focus:outline-none focus:ring-2 focus:ring-brand-500/40 transition-all" + placeholder="Doe" + required + /> +
+
+ +
+ + setInstituteName(e.target.value)} + className="w-full p-3 text-sm bg-theme-tertiary border border-theme-primary rounded-xl text-theme-primary focus:outline-none focus:ring-2 focus:ring-brand-500/40 transition-all" + placeholder="Where do you study/work?" + required + /> +
+ +
+ + setLocation(e.target.value)} + className="w-full p-3 text-sm bg-theme-tertiary border border-theme-primary rounded-xl text-theme-primary focus:outline-none focus:ring-2 focus:ring-brand-500/40 transition-all" + placeholder="City, Country" + /> +
+ + +
+
+
+ ); +}; + +export default OnboardingModal; diff --git a/app/(root)/components/ProfileCard.tsx b/app/(root)/components/ProfileCard.tsx new file mode 100644 index 0000000..1792939 --- /dev/null +++ b/app/(root)/components/ProfileCard.tsx @@ -0,0 +1,109 @@ +"use client"; +import React from "react"; +import Badge from "./Badge"; +import Signature from "./Signature"; +import { FaGithub, FaLinkedin } from "react-icons/fa"; +import { User } from "@/app/types/user"; +import { MapPin, Building, Code2, Trophy } from "lucide-react"; + +const ProfileCard = ({ profileData }: { profileData: User }) => { + return ( +
+
+ {/* Left: Badge + Signature */} +
+ + +
+ + {/* Right: Info */} +
+

+ {profileData.name || `${profileData.firstName} ${profileData.lastName}`} +

+ +
+
+ +
+ Projects Completed: + + {profileData.projectsCompleted ?? 0} + +
+
+ +
+ +
+ Location: + {profileData.location || "Not set"} +
+
+ +
+ +
+ Institute: + + {profileData.collegeDetails?.name || "Not set"} + +
+
+ +
+ +
+ Skills: +
+ {profileData.skills && profileData.skills.length > 0 ? ( + profileData.skills.map((skill, i) => ( + + {skill} + + )) + ) : ( + No skills added + )} +
+
+
+
+ + {/* Social Links */} +
+ Socials: + {profileData.github && ( + + + + )} + {profileData.linkedin && ( + + + + )} + {!profileData.github && !profileData.linkedin && ( + No links added + )} +
+
+
+
+ ); +}; + +export default ProfileCard; diff --git a/app/(root)/components/SignInButton.tsx b/app/(root)/components/SignInButton.tsx new file mode 100644 index 0000000..91eb936 --- /dev/null +++ b/app/(root)/components/SignInButton.tsx @@ -0,0 +1,28 @@ +"use client"; +import { signIn } from "next-auth/react"; +import { useSession } from "next-auth/react"; + +export default function SignInButton() { + const { status } = useSession(); + + if (status === "loading") { + return ( + + ); + } + + if (status === "authenticated") { + return null; + } + + return ( + + ); +} diff --git a/app/(root)/components/SignOutButton.tsx b/app/(root)/components/SignOutButton.tsx new file mode 100644 index 0000000..8de42c8 --- /dev/null +++ b/app/(root)/components/SignOutButton.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { signOut } from "next-auth/react"; +import { useSession } from "next-auth/react"; +import { LogOut } from "lucide-react"; + +export default function SignOutButton() { + const { status } = useSession(); + + if (status === "loading") { + return ( + + ); + } + + if (status === "unauthenticated") { + return null; + } + + return ( + + ); +} diff --git a/app/components/Signature.tsx b/app/(root)/components/Signature.tsx similarity index 51% rename from app/components/Signature.tsx rename to app/(root)/components/Signature.tsx index 3141242..b42a82f 100644 --- a/app/components/Signature.tsx +++ b/app/(root)/components/Signature.tsx @@ -2,6 +2,7 @@ import Image from "next/image"; import React, { useState } from "react"; +import { Upload, ImageIcon } from "lucide-react"; const SignatureCard = () => { const [image, setImage] = useState(null); @@ -16,32 +17,32 @@ const SignatureCard = () => { }; return ( -
- {/* Image Preview */} +
{image ? ( Signature Preview ) : ( -
-

No Signature Selected

+
+ +

No signature

)} - {/* File Input & Upload Button */} -