diff --git a/frontend/app/api/auth/dashboard/activities/route.ts b/frontend/app/api/auth/dashboard/activities/route.ts new file mode 100644 index 0000000..a3064c7 --- /dev/null +++ b/frontend/app/api/auth/dashboard/activities/route.ts @@ -0,0 +1,71 @@ +import { getCurrentUser } from "@/lib/auth" +import prisma from "@/lib/prisma" +import { NextResponse } from "next/server" + +export async function GET(request: Request) { + try { + const user = await getCurrentUser() + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + if (user.role === "APPLICANT") { + // Fetch recent applications (last 5) + const recentApplications = await prisma.internshipApplication.findMany({ + where: { applicantId: user.id }, + include: { internship: true }, + orderBy: { createdAt: "desc" }, + take: 5, + }) + + const activities = recentApplications.map((app) => ({ + icon: getStatusIcon(app.status), + title: `Application ${getStatusText(app.status)}`, + description: app.internship.title, + date: new Date(app.createdAt).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), + })) + + return NextResponse.json({ activities }) + } + + return NextResponse.json({ activities: [] }) + } catch (error) { + console.error("[v0] Activities error:", error) + return NextResponse.json({ error: "Failed to fetch activities" }, { status: 500 }) + } +} + +function getStatusIcon(status: string): string { + switch (status) { + case "ACCEPTED": + return "✅" + case "REJECTED": + return "❌" + case "PENDING": + return "⏳" + case "REVIEWED": + return "👀" + default: + return "📝" + } +} + +function getStatusText(status: string): string { + switch (status) { + case "ACCEPTED": + return "Accepted" + case "REJECTED": + return "Rejected" + case "PENDING": + return "Submitted" + case "REVIEWED": + return "Under Review" + default: + return "Created" + } +} diff --git a/frontend/app/api/auth/dashboard/activity-chart/route.ts b/frontend/app/api/auth/dashboard/activity-chart/route.ts new file mode 100644 index 0000000..be5dd82 --- /dev/null +++ b/frontend/app/api/auth/dashboard/activity-chart/route.ts @@ -0,0 +1,81 @@ +import { getCurrentUser } from "@/lib/auth" +import prisma from "@/lib/prisma" +import { NextResponse } from "next/server" + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + if (user.role === "APPLICANT") { + // Get applications from the last 6 months + const sixMonthsAgo = new Date() + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6) + + const applications = await prisma.internshipApplication.findMany({ + where: { + applicantId: user.id, + createdAt: { + gte: sixMonthsAgo, + }, + }, + select: { + createdAt: true, + }, + orderBy: { + createdAt: "asc", + }, + }) + + // Group by month + const monthlyData = new Map() + const months = [] + + // Initialize last 6 months + for (let i = 5; i >= 0; i--) { + const date = new Date() + date.setMonth(date.getMonth() - i) + const monthKey = date.toLocaleDateString("en-US", { month: "short" }) + months.push(monthKey) + monthlyData.set(monthKey, 0) + } + + // Count applications per month + applications.forEach((app) => { + const monthKey = app.createdAt.toLocaleDateString("en-US", { month: "short" }) + if (monthlyData.has(monthKey)) { + monthlyData.set(monthKey, (monthlyData.get(monthKey) || 0) + 1) + } + }) + + // Format for chart + const chartData = months.map((month) => ({ + month, + applications: monthlyData.get(month) || 0, + })) + + return NextResponse.json({ chartData }) + } + + const months = [] + for (let i = 5; i >= 0; i--) { + const date = new Date() + date.setMonth(date.getMonth() - i) + const monthKey = date.toLocaleDateString("en-US", { month: "short" }) + months.push(monthKey) + } + + const chartData = months.map((month) => ({ + month, + applications: 0, + })) + + return NextResponse.json({ chartData }) + } catch (error) { + console.error("[v0] Activity chart error:", error) + return NextResponse.json({ error: "Failed to fetch activity data" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/dashboard/recruiter-activities/route.ts b/frontend/app/api/auth/dashboard/recruiter-activities/route.ts new file mode 100644 index 0000000..d394eff --- /dev/null +++ b/frontend/app/api/auth/dashboard/recruiter-activities/route.ts @@ -0,0 +1,45 @@ +import { getCurrentUser } from "@/lib/auth" +import { NextResponse } from "next/server" + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user || user.role !== "RECRUITER") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + // Mock recent activities for recruiters + const activities = [ + { + icon: "✅", + title: "Candidate Hired", + description: "John Smith accepted offer for Frontend Developer position", + date: "2 hours ago", + }, + { + icon: "📝", + title: "New Application", + description: "Sarah Johnson applied for Backend Engineer Intern", + date: "5 hours ago", + }, + { + icon: "📅", + title: "Interview Scheduled", + description: "Interview with Michael Chen for UI/UX Design Intern", + date: "1 day ago", + }, + { + icon: "📢", + title: "Listing Published", + description: "Full Stack Developer Intern position is now live", + date: "2 days ago", + }, + ] + + return NextResponse.json({ activities }) + } catch (error) { + console.error("[v0] Recruiter activities error:", error) + return NextResponse.json({ error: "Failed to fetch activities" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/dashboard/recruiter-activity-chart/route.ts b/frontend/app/api/auth/dashboard/recruiter-activity-chart/route.ts new file mode 100644 index 0000000..dd8fe74 --- /dev/null +++ b/frontend/app/api/auth/dashboard/recruiter-activity-chart/route.ts @@ -0,0 +1,28 @@ +import { getCurrentUser } from "@/lib/auth" +import { NextResponse } from "next/server" + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user || user.role !== "RECRUITER") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + // Mock data for recruiter activity chart + // In production, this would aggregate data from internship applications + const chartData = [ + { month: "Jul", applications: 45, hires: 2 }, + { month: "Aug", applications: 62, hires: 3 }, + { month: "Sep", applications: 78, hires: 5 }, + { month: "Oct", applications: 91, hires: 4 }, + { month: "Nov", applications: 103, hires: 6 }, + { month: "Dec", applications: 87, hires: 3 }, + ] + + return NextResponse.json(chartData) + } catch (error) { + console.error("[v0] Recruiter activity chart error:", error) + return NextResponse.json({ error: "Failed to fetch chart data" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/dashboard/recruiter-stats/route.ts b/frontend/app/api/auth/dashboard/recruiter-stats/route.ts new file mode 100644 index 0000000..01b5cbf --- /dev/null +++ b/frontend/app/api/auth/dashboard/recruiter-stats/route.ts @@ -0,0 +1,33 @@ +import { getCurrentUser } from "@/lib/auth" +import { NextResponse } from "next/server" + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user || user.role !== "RECRUITER") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + // Mock data for recruiter stats + // In production, these would query actual internship and application tables + const activeListings = 12 + const totalApplicants = 247 + const hiredCandidates = 8 + const shortlistedCandidates = 23 + const pendingReviews = 45 + const upcomingInterviews = 15 + + return NextResponse.json({ + activeListings, + totalApplicants, + hiredCandidates, + shortlistedCandidates, + pendingReviews, + upcomingInterviews, + }) + } catch (error) { + console.error("[v0] Recruiter stats error:", error) + return NextResponse.json({ error: "Failed to fetch stats" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/dashboard/stats/route.ts b/frontend/app/api/auth/dashboard/stats/route.ts new file mode 100644 index 0000000..ed00cdc --- /dev/null +++ b/frontend/app/api/auth/dashboard/stats/route.ts @@ -0,0 +1,74 @@ +import { getCurrentUser } from "@/lib/auth" +import prisma from "@/lib/prisma" +import { NextResponse } from "next/server" + +export async function GET() { + try { + const user = await getCurrentUser() + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }) + } + + if (user.role === "APPLICANT") { + // Total applications submitted + const totalApplications = await prisma.internshipApplication.count({ + where: { applicantId: user.id }, + }) + + // Applications by status + const applicationsByStatus = await prisma.internshipApplication.groupBy({ + by: ["status"], + where: { applicantId: user.id }, + _count: true, + }) + + const acceptedCount = applicationsByStatus.find((a) => a.status === "ACCEPTED")?._count || 0 + const pendingCount = applicationsByStatus.find((a) => a.status === "PENDING")?._count || 0 + const reviewedCount = applicationsByStatus.find((a) => a.status === "REVIEWED")?._count || 0 + + // Get last application created date + const lastApplication = await prisma.internshipApplication.findFirst({ + where: { applicantId: user.id }, + orderBy: { createdAt: "desc" }, + select: { createdAt: true }, + }) + + // Get applicant details + const applicant = await prisma.applicant.findUnique({ + where: { userId: user.id }, + select: { + linkedInLink: true, + portfolioLink: true, + githubLink: true, + }, + }) + + const savedInternships = applicant?.linkedInLink ? 1 : 0 + const upcomingInterviews = 0 + + return NextResponse.json({ + totalApplications, + acceptedApplications: acceptedCount, + reviewedApplications: reviewedCount, + pendingApplications: pendingCount, + savedInternships, + upcomingInterviews, + lastActivityDate: lastApplication?.createdAt || user.emailVerified || new Date(), + }) + } + + return NextResponse.json({ + totalApplications: 0, + acceptedApplications: 0, + reviewedApplications: 0, + pendingApplications: 0, + savedInternships: 0, + upcomingInterviews: 0, + lastActivityDate: new Date(), + }) + } catch (error) { + console.error("[v0] Dashboard stats error:", error) + return NextResponse.json({ error: "Failed to fetch stats" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/profile/GitHubAttach/route.ts b/frontend/app/api/auth/profile/GitHubAttach/route.ts index ec66684..895fb18 100644 --- a/frontend/app/api/auth/profile/GitHubAttach/route.ts +++ b/frontend/app/api/auth/profile/GitHubAttach/route.ts @@ -1,41 +1,40 @@ import { NextRequest, NextResponse } from "next/server"; import { getGitHubUser } from "@/lib/GithubAPI"; -import { cookies } from "next/headers"; import prisma from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; export async function POST(req: NextRequest) { console.log("Received request to attach GitHub profile"); + try { - const sessionToken = cookies().get("sessionToken")?.value; - if (!sessionToken) { - return NextResponse.json({ error: "No session found" }, { status: 401 }); - } + // Authenticate the request + const currentUser = await requireAuth(); - // Find user from Prisma using sessionToken - const session = await prisma.session.findUnique({ - where: { sessionToken }, - include: { user: true }, - }); + // Parse request body + const { githubLink, userId } = await req.json(); - if (!session || !session.user) { - return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 }); + if (!githubLink) { + return NextResponse.json( + { error: "GitHub username is required" }, + { status: 400 } + ); } - const userId = session.user.id; - const { username } = await req.json(); + if (!userId) { + return NextResponse.json({ error: "Missing userId" }, { status: 400 }); + } - console.log("Request body:", { username }); - if (!username) { + // Prevent users from editing others' accounts + if (currentUser.id !== userId) { return NextResponse.json( - { error: "Username is required" }, - { status: 400 } + { error: "Forbidden: user mismatch" }, + { status: 403 } ); } - console.log("Received GitHub username:", username); - // Clean up username if user pasted full GitHub URL - const cleanUsername = username + // Clean up username if a full GitHub URL was pasted + const cleanUsername = githubLink .trim() .replace(/^https?:\/\/(www\.)?github\.com\//, "") .split("/")[0]; @@ -44,25 +43,32 @@ export async function POST(req: NextRequest) { // Fetch GitHub user data const githubUser = await getGitHubUser(cleanUsername); - const githubLink = `https://github.com/${cleanUsername}`; + const fullLink = `https://github.com/${cleanUsername}`; // Save to Applicant model const updatedApplicant = await prisma.applicant.upsert({ where: { userId }, update: { githubLink }, - create: { - userId, - githubLink, - }, + create: { userId, githubLink }, + select: { id: true, githubLink: true }, }); return NextResponse.json({ - message: "GitHub data fetched and saved successfully", + message: "GitHub profile linked successfully", githubData: githubUser, applicant: updatedApplicant, }); - } catch (err: any) { - console.error("Error fetching GitHub data:", err); - return NextResponse.json({ error: err.message }, { status: 500 }); + } catch (error: unknown) { + console.error("Error attaching GitHub profile:", error); + + let message = "Internal Server Error"; + let status = 500; + + if (error instanceof Error) { + message = error.message; + if (message.includes("Unauthorized")) status = 401; + } + + return NextResponse.json({ error: message }, { status }); } } diff --git a/frontend/app/api/auth/profile/delete-github-link/route.ts b/frontend/app/api/auth/profile/delete-github-link/route.ts new file mode 100644 index 0000000..1b52d1a --- /dev/null +++ b/frontend/app/api/auth/profile/delete-github-link/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; + +export async function DELETE(req: Request) { + try { + const currentUser = await requireAuth(); + + const { userId, field, value } = await req.json(); + + if (currentUser.id !== userId) { + return NextResponse.json({ error: "Forbidden: user mismatch" }, { status: 403 }); + } + + if (!userId || field !== "githubLink") { + return NextResponse.json({ error: "Invalid request" }, { status: 400 }); + } + + const updatedApplicant = await prisma.applicant.update({ + where: { userId }, + data: { githubLink: value }, + select: { id: true, githubLink: true }, + }); + + return NextResponse.json({ + message: "GitHub link removed successfully", + applicant: updatedApplicant, + }); + } catch (error: unknown) { + console.error("Error removing GitHub link:", error); + + let message = "Internal Server Error"; + let status = 500; + + if (error instanceof Error) { + message = error.message; + if (message.includes("Unauthorized")) status = 401; + } + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/frontend/app/api/auth/profile/delete-linkedin-link/route.ts b/frontend/app/api/auth/profile/delete-linkedin-link/route.ts new file mode 100644 index 0000000..009e104 --- /dev/null +++ b/frontend/app/api/auth/profile/delete-linkedin-link/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; + +export async function DELETE(req: Request) { + try { + const currentUser = await requireAuth(); + + const { userId, field, value } = await req.json(); + + if (currentUser.id !== userId) { + return NextResponse.json({ error: "Forbidden: user mismatch" }, { status: 403 }); + } + + if (!userId || field !== "linkedInLink") { + return NextResponse.json({ error: "Invalid request" }, { status: 400 }); + } + + const updatedApplicant = await prisma.applicant.update({ + where: { userId }, + data: { linkedInLink: value }, + select: { id: true, linkedInLink: true }, + }); + + return NextResponse.json({ + message: "LinkedIn link removed successfully", + applicant: updatedApplicant, + }); + } catch (error: unknown) { + console.error("Error removing LinkedIn link:", error); + + let message = "Internal Server Error"; + let status = 500; + + if (error instanceof Error) { + message = error.message; + if (message.includes("Unauthorized")) status = 401; + } + + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/frontend/app/api/auth/recruiter/internships/route.ts b/frontend/app/api/auth/recruiter/internships/route.ts new file mode 100644 index 0000000..2be0e15 --- /dev/null +++ b/frontend/app/api/auth/recruiter/internships/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server" + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const recruiterId = searchParams.get("recruiterId") + + if (!recruiterId) { + return NextResponse.json({ error: "Recruiter ID required" }, { status: 400 }) + } + + // Mock data for recruiter internships + // In production, this would query the actual internships table + const mockInternships = [ + { + id: "1", + title: "Frontend Developer Intern", + description: + "Join our team to build modern web applications using React and TypeScript. You'll work on real projects and learn from experienced developers.", + location: "Remote", + type: "Full-time", + status: "ACTIVE", + applicantsCount: 45, + }, + { + id: "2", + title: "Backend Engineer Intern", + description: + "Work with our backend team to develop scalable APIs and microservices. Experience with Node.js and databases preferred.", + location: "San Francisco, CA", + type: "Part-time", + status: "ACTIVE", + applicantsCount: 32, + }, + { + id: "3", + title: "UI/UX Design Intern", + description: + "Help design beautiful and intuitive user interfaces. You'll collaborate with product managers and developers to create amazing user experiences.", + location: "New York, NY", + type: "Full-time", + status: "ACTIVE", + applicantsCount: 28, + }, + ] + + return NextResponse.json({ internships: mockInternships }) + } catch (error) { + console.error("[v0] Recruiter internships error:", error) + return NextResponse.json({ error: "Failed to fetch internships" }, { status: 500 }) + } +} diff --git a/frontend/app/api/auth/signup/route.ts b/frontend/app/api/auth/signup/route.ts index a887f7b..5ddb8a2 100644 --- a/frontend/app/api/auth/signup/route.ts +++ b/frontend/app/api/auth/signup/route.ts @@ -5,7 +5,6 @@ import crypto from "node:crypto"; import nodemailer from "nodemailer"; export async function POST(req: NextRequest) { - console.log("runnig"); try { const { name, email, password, role } = await req.json(); diff --git a/frontend/app/dashboard/[userId]/page.tsx b/frontend/app/dashboard/[userId]/page.tsx new file mode 100644 index 0000000..965b1aa --- /dev/null +++ b/frontend/app/dashboard/[userId]/page.tsx @@ -0,0 +1,33 @@ +import { requireAuth } from "@/lib/auth" +import { redirect } from "next/navigation" +import DashboardHeader from "@/components/dashboard/DashboardHeader" +import StatsGrid from "@/components/dashboard/StatsGrid" +import RecentActivityCard from "@/components/dashboard/RecentActivityCard" +import ActivityChart from "@/components/dashboard/ActivityChart" +import SuccessGauge from "@/components/dashboard/SuccessGauge" + +export default async function DashboardPage() { + const user = await requireAuth() + + if (!user || user.role !== "APPLICANT") { + redirect("/") + } + + return ( +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ ) +} diff --git a/frontend/app/profile/[Id]/page.tsx b/frontend/app/profile/[Id]/page.tsx index 01a732b..ef20b02 100644 --- a/frontend/app/profile/[Id]/page.tsx +++ b/frontend/app/profile/[Id]/page.tsx @@ -10,6 +10,9 @@ import ContactSection from "@/components/profile/ContactSection" import GitHubSection from "@/components/profile/GitHubSection" import LinkedInSection from "@/components/profile/LinkedInSection" import ResumeSection from "@/components/profile/ResumeSection" +import RecruiterCompanySection from "@/components/profile/RecruiterCompanySection" +import RecruiterContactSection from "@/components/profile/RecruiterContactSection" +import RecruiterListingsSection from "@/components/profile/RecruiterListingsSection" export default async function ProfilePage({ params }: { params: { Id: string } }) { const loggedInUser = await getCurrentUser() @@ -18,7 +21,11 @@ export default async function ProfilePage({ params }: { params: { Id: string } } where: { id: params.Id }, include: { applicant: true, - recruiter: true, + recruiter: { + include: { + company: true, + }, + }, }, }) @@ -35,7 +42,9 @@ export default async function ProfilePage({ params }: { params: { Id: string } } const isOwner = loggedInUser?.id === profileUser.id const isApplicant = profileUser.role === "APPLICANT" + const isRecruiter = profileUser.role === "RECRUITER" const applicant = profileUser.applicant + const recruiter = profileUser.recruiter const hasResume = !!applicant?.resumeLink const hasGitHub = !!applicant?.githubLink @@ -78,64 +87,90 @@ export default async function ProfilePage({ params }: { params: { Id: string } } hasResume={hasResume} hasGitHub={hasGitHub} hasLinkedIn={hasLinkedIn} + isRecruiter={isRecruiter} + recruiter={recruiter} /> {/* Main Content */}
- - - - Projects & Skills - - - Experience & Contact - - - Integrations - - + {isApplicant && ( + + + + Projects & Skills + + + Experience & Contact + + + Integrations + + - {/* Projects & Skills Tab */} - - - - + {/* Projects & Skills Tab */} + + + + - {/* Experience & Contact Tab */} - - - - + {/* Experience & Contact Tab */} + + + + - {/* Integrations Tab */} - - - - - - + {/* Integrations Tab */} + + + + + + + )} + + {isRecruiter && ( + + + + Company Info + + + Active Listings + + + + + + + + + + + + + )}
diff --git a/frontend/app/student_dashboard/page.tsx b/frontend/app/student_dashboard/page.tsx deleted file mode 100644 index 41613ab..0000000 --- a/frontend/app/student_dashboard/page.tsx +++ /dev/null @@ -1,266 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { - Search, - MapPin, - Calendar, - DollarSign, - Clock, - Briefcase, - LogOut, - Filter, - Building2, - BookmarkPlus, - TrendingUp, -} from "lucide-react"; -import { useRouter } from "next/navigation"; - -const Dashboard = () => { - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const [locationFilter, setLocationFilter] = useState("all"); - const [typeFilter, setTypeFilter] = useState("all"); - const [durationFilter, setDurationFilter] = useState("all"); - - const user = { - name: "John Doe", - email: "john.doe@example.com", - role: "applicant", - }; - - const [internships, setInternships] = useState([]); -const [loading, setLoading] = useState(true); -const [fetchError, setFetchError] = useState(null); - - useEffect(() => { - const fetchInternships = async () => { - setLoading(true); - setFetchError(null); - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/internships`); - if (!res.ok) throw new Error("Failed to fetch"); - const data = await res.json(); - setInternships(data); - } catch (err) { - setFetchError("Error loading internships"); - } finally { - setLoading(false); - } - }; - - fetchInternships(); -}, []); - - - const filteredInternships = internships.filter((internship) => { - const matchesSearch = - internship.title.toLowerCase().includes(searchQuery.toLowerCase()) || - internship.company.toLowerCase().includes(searchQuery.toLowerCase()) || - internship.skills.some((skill) => - skill.toLowerCase().includes(searchQuery.toLowerCase()) - ); - const matchesLocation = - locationFilter === "all" || internship.location === locationFilter; - const matchesType = typeFilter === "all" || internship.type === typeFilter; - const matchesDuration = - durationFilter === "all" || internship.duration === durationFilter; - - return matchesSearch && matchesLocation && matchesType && matchesDuration; - }); - - return ( -
- {/* Header */} -
-
-
-
-
- -
-

Kairo

-
- -
- - - -
-
-
-
- - {/* Main Content */} -
- {/* Search & Filters */} -
-
-
-

- - Find Your Perfect Internship -

-

- Search and filter through available opportunities -

-
- -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all" - /> -
- -
- - - - - -
- -
-

- {filteredInternships.length} internship - {filteredInternships.length !== 1 ? "s" : ""} found -

- -
-
-
-
- - {/* No internships yet */} - - {loading &&

Loading internships...

} - -{fetchError && ( -

{fetchError}

-)} - -{!loading && filteredInternships.length > 0 && ( -
- {filteredInternships.map((internship) => ( -
router.push(`/student_dashboard/internship/${internship._id}`)} // ✅ Dynamic route - className="bg-white shadow-md rounded-lg p-6 border border-gray-200 hover:shadow-lg transition cursor-pointer" - > -

{internship.title}

-

{internship.company}

-

{internship.location}

-
- ))} -
-)} - -{!loading && filteredInternships.length === 0 && ( -
- -

No internships found

-

Try adjusting your filters or search query

-
-)} -
- - {/* Animations */} - -
- ); -}; - -export default Dashboard; diff --git a/frontend/components/GitHubDeleteButton.tsx b/frontend/components/GitHubDeleteButton.tsx new file mode 100644 index 0000000..b06c702 --- /dev/null +++ b/frontend/components/GitHubDeleteButton.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Trash2 } from "lucide-react" + +interface GitHubDeleteButtonProps { + userId: string + onDelete: () => void +} + +export default function GitHubDeleteButton({ userId, onDelete }: GitHubDeleteButtonProps) { + const [isDeleting, setIsDeleting] = useState(false) + + const handleDelete = async () => { + if (!confirm("Are you sure you want to remove your GitHub integration?")) return + + setIsDeleting(true) + try { + const response = await fetch("/api/auth/profile/delete-github-link", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId, + field: "githubLink", + value: null, + }), + }) + + if (response.ok) { + onDelete() + } else { + alert("Failed to remove GitHub link") + } + } catch (error) { + console.error("Error deleting GitHub link:", error) + alert("An error occurred while removing GitHub link") + } finally { + setIsDeleting(false) + } + } + + return ( + + ) +} diff --git a/frontend/components/GithubButton.tsx b/frontend/components/GithubButton.tsx index 62f8354..dd0ce25 100644 --- a/frontend/components/GithubButton.tsx +++ b/frontend/components/GithubButton.tsx @@ -11,7 +11,6 @@ export default function GithubButton({ userId, currentLink }: { userId: string; const handleSave = async () => { if (!githubLink.trim()) return - setLoading(true) setError("") try { diff --git a/frontend/components/LinkedInDeleteButton.tsx b/frontend/components/LinkedInDeleteButton.tsx new file mode 100644 index 0000000..b485585 --- /dev/null +++ b/frontend/components/LinkedInDeleteButton.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Trash2 } from "lucide-react" + +interface LinkedInDeleteButtonProps { + userId: string + onDelete: () => void +} + +export default function LinkedInDeleteButton({ userId, onDelete }: LinkedInDeleteButtonProps) { + const [isDeleting, setIsDeleting] = useState(false) + + const handleDelete = async () => { + if (!confirm("Are you sure you want to remove your LinkedIn integration?")) return + + setIsDeleting(true) + try { + const response = await fetch("/api/auth/profile/delete-linkedin-link", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId, + field: "linkedInLink", + value: null, + }), + }) + + if (response.ok) { + onDelete() + } else { + alert("Failed to remove LinkedIn link") + } + } catch (error) { + console.error("Error deleting LinkedIn link:", error) + alert("An error occurred while removing LinkedIn link") + } finally { + setIsDeleting(false) + } + } + + return ( + + ) +} diff --git a/frontend/components/LinkedinButton.tsx b/frontend/components/LinkedinButton.tsx index 78d83d9..49dff12 100644 --- a/frontend/components/LinkedinButton.tsx +++ b/frontend/components/LinkedinButton.tsx @@ -5,12 +5,12 @@ import { useState } from "react" export default function LinkedinButton({ userId, currentLink }: { userId: string; currentLink?: string }) { const [isEditing, setIsEditing] = useState(false) - const [linkedinLink, setLinkedinLink] = useState(currentLink || "") + const [linkedin, setLinkedinLink] = useState(currentLink || "") const [loading, setLoading] = useState(false) const [error, setError] = useState("") const handleSave = async () => { - if (!linkedinLink.trim()) return + if (!linkedin.trim()) return setLoading(true) setError("") @@ -18,7 +18,7 @@ export default function LinkedinButton({ userId, currentLink }: { userId: string const response = await fetch("/api/auth/profile/LinkedInAttach", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userId, linkedinLink }), + body: JSON.stringify({ userId, linkedin }), }) if (!response.ok) throw new Error("Failed to update LinkedIn link") @@ -45,7 +45,7 @@ export default function LinkedinButton({ userId, currentLink }: { userId: string
setLinkedinLink(e.target.value)} placeholder="https://linkedin.com/in/username" className="w-full px-3 py-2 border rounded-md text-sm" diff --git a/frontend/components/dashboard/ActivityChart.tsx b/frontend/components/dashboard/ActivityChart.tsx new file mode 100644 index 0000000..afc94c4 --- /dev/null +++ b/frontend/components/dashboard/ActivityChart.tsx @@ -0,0 +1,90 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Area, AreaChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts" +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" + +interface ActivityChartProps { + userId: string +} + +export default function ActivityChart({ userId }: ActivityChartProps) { + const [chartData, setChartData] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchActivityData = async () => { + try { + const response = await fetch(`/api/auth/dashboard/activity-chart?userId=${userId}`) + if (response.ok) { + const data = await response.json() + setChartData(data.chartData) + } + } catch (error) { + console.error("[v0] Error fetching activity chart:", error) + } finally { + setLoading(false) + } + } + + fetchActivityData() + }, [userId]) + + if (loading) { + return ( + + + Application Activity + Your application trends over time + + +
+ + + ) + } + + return ( + + + Application Activity + Your application trends over time + + + + + + + + + + + + + + + } /> + + + + + + + ) +} diff --git a/frontend/components/dashboard/DashboardCard.tsx b/frontend/components/dashboard/DashboardCard.tsx new file mode 100644 index 0000000..46b4d70 --- /dev/null +++ b/frontend/components/dashboard/DashboardCard.tsx @@ -0,0 +1,26 @@ +"use client" + +interface DashboardCardProps { + icon: string + label: string + value: string | number + description: string + isDate?: boolean +} + +export default function DashboardCard({ icon, label, value, description, isDate }: DashboardCardProps) { + return ( +
+
+
+
+ {icon} +

{label}

+
+

{value}

+

{description}

+
+
+
+ ) +} diff --git a/frontend/components/dashboard/DashboardHeader.tsx b/frontend/components/dashboard/DashboardHeader.tsx new file mode 100644 index 0000000..f6773f3 --- /dev/null +++ b/frontend/components/dashboard/DashboardHeader.tsx @@ -0,0 +1,25 @@ +"use client" + +interface DashboardHeaderProps { + user: any +} + +export default function DashboardHeader({ user }: DashboardHeaderProps) { + const greeting = getGreeting() + + return ( +
+

+ {greeting}, {user.name || "there"}! +

+

Here's your application journey at a glance

+
+ ) +} + +function getGreeting() { + const hour = new Date().getHours() + if (hour < 12) return "Good morning" + if (hour < 18) return "Good afternoon" + return "Good evening" +} diff --git a/frontend/components/dashboard/RecentActivityCard.tsx b/frontend/components/dashboard/RecentActivityCard.tsx new file mode 100644 index 0000000..5fb0c51 --- /dev/null +++ b/frontend/components/dashboard/RecentActivityCard.tsx @@ -0,0 +1,84 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" + +interface RecentActivityCardProps { + userId: string + isRecruiter?: boolean +} + +export default function RecentActivityCard({ userId, isRecruiter = false }: RecentActivityCardProps) { + const [activities, setActivities] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchActivities = async () => { + try { + const endpoint = isRecruiter + ? `/api/auth/dashboard/recruiter-activities?userId=${userId}` + : `/api/auth/dashboard/activities?userId=${userId}` + const response = await fetch(endpoint) + if (response.ok) { + const data = await response.json() + setActivities(data.activities) + } + } catch (error) { + console.error("Error fetching activities:", error) + } finally { + setLoading(false) + } + } + + fetchActivities() + }, [userId, isRecruiter]) + + if (loading) { + return ( + + + Recent Activity + + {isRecruiter ? "Latest recruitment activities" : "Your latest submissions and updates"} + + + +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ + + ) + } + + return ( + + + Recent Activity + + {isRecruiter ? "Latest recruitment activities" : "Your latest submissions and updates"} + + + + {activities.length === 0 ? ( +

No recent activity yet

+ ) : ( +
+ {activities.map((activity, index) => ( +
+
{activity.icon}
+
+

{activity.title}

+

{activity.description}

+

{activity.date}

+
+
+ ))} +
+ )} +
+
+ ) +} diff --git a/frontend/components/dashboard/RecruiterActivityChart.tsx b/frontend/components/dashboard/RecruiterActivityChart.tsx new file mode 100644 index 0000000..8e78538 --- /dev/null +++ b/frontend/components/dashboard/RecruiterActivityChart.tsx @@ -0,0 +1,112 @@ +"use client" + +import { useEffect, useState } from "react" +import { Area, AreaChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" + +interface ActivityData { + month: string + applications: number + hires: number +} + +interface RecruiterActivityChartProps { + userId: string +} + +export default function RecruiterActivityChart({ userId }: RecruiterActivityChartProps) { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetch("/api/auth/dashboard/recruiter-activity-chart") + if (response.ok) { + const chartData = await response.json() + setData(chartData) + } + } catch (error) { + console.error("[v0] Error fetching recruiter activity chart:", error) + } finally { + setLoading(false) + } + } + + fetchData() + }, []) + + if (loading) { + return ( + + + Recruitment Activity + Applications received and hires made over time + + +
+ + + ) + } + + return ( + + + Recruitment Activity + Applications received and hires made over the last 6 months + + + + + + + + + + + + + + + + + + + } /> + + + + + + + + ) +} diff --git a/frontend/components/dashboard/RecruiterStatsGrid.tsx b/frontend/components/dashboard/RecruiterStatsGrid.tsx new file mode 100644 index 0000000..1a13a6e --- /dev/null +++ b/frontend/components/dashboard/RecruiterStatsGrid.tsx @@ -0,0 +1,95 @@ +"use client" + +import { useEffect, useState } from "react" +import DashboardCard from "./DashboardCard" + +interface RecruiterStatsData { + activeListings: number + totalApplicants: number + hiredCandidates: number + shortlistedCandidates: number + pendingReviews: number + upcomingInterviews: number +} + +interface RecruiterStatsGridProps { + userId: string +} + +export default function RecruiterStatsGrid({ userId }: RecruiterStatsGridProps) { + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch("/api/auth/dashboard/recruiter-stats") + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (error) { + console.error("[v0] Error fetching recruiter stats:", error) + } finally { + setLoading(false) + } + } + + fetchStats() + }, []) + + if (loading) { + return ( +
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+ ) + } + + if (!stats) { + return
Failed to load stats
+ } + + return ( +
+ + + + + + +
+ ) +} diff --git a/frontend/components/dashboard/StatsGrid.tsx b/frontend/components/dashboard/StatsGrid.tsx new file mode 100644 index 0000000..b15e866 --- /dev/null +++ b/frontend/components/dashboard/StatsGrid.tsx @@ -0,0 +1,104 @@ +"use client" + +import { useEffect, useState } from "react" +import DashboardCard from "./DashboardCard" + +interface StatsData { + totalApplications: number + acceptedApplications: number + reviewedApplications: number + pendingApplications: number + savedInternships: number + upcomingInterviews: number + lastActivityDate: string +} + +interface StatsGridProps { + userId: string +} + +export default function StatsGrid({ userId }: StatsGridProps) { + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch("/api/auth/dashboard/stats") + if (response.ok) { + const data = await response.json() + setStats(data) + } + } catch (error) { + console.error("[v0] Error fetching stats:", error) + } finally { + setLoading(false) + } + } + + fetchStats() + }, []) + + if (loading) { + return ( +
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+ ) + } + + if (!stats) { + return
Failed to load stats
+ } + + const lastActivityDate = new Date(stats.lastActivityDate) + const formattedDate = lastActivityDate.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + + return ( +
+ + + + + + +
+ ) +} diff --git a/frontend/components/dashboard/SuccessGauge.tsx b/frontend/components/dashboard/SuccessGauge.tsx new file mode 100644 index 0000000..1413fe5 --- /dev/null +++ b/frontend/components/dashboard/SuccessGauge.tsx @@ -0,0 +1,114 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" + +interface SuccessGaugeProps { + userId: string +} + +export default function SuccessGauge({ userId }: SuccessGaugeProps) { + const [successRate, setSuccessRate] = useState(0) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchSuccessRate = async () => { + try { + const response = await fetch("/api/auth/dashboard/stats") + if (response.ok) { + const data = await response.json() + const rate = + data.totalApplications > 0 ? Math.round((data.acceptedApplications / data.totalApplications) * 100) : 0 + setSuccessRate(rate) + } + } catch (error) { + console.error("[v0] Error fetching success rate:", error) + } finally { + setLoading(false) + } + } + + fetchSuccessRate() + }, [userId]) + + if (loading) { + return ( + + + Success Rate + Application acceptance rate + + +
+ + + ) + } + + // Calculate gauge rotation (0-180 degrees) + const rotation = (successRate / 100) * 180 + + // Determine color based on success rate + const getColor = () => { + if (successRate >= 70) return "hsl(var(--chart-2))" // Green + if (successRate >= 40) return "hsl(var(--chart-3))" // Yellow + return "hsl(var(--chart-4))" // Red + } + + return ( + + + Success Rate + Application acceptance rate + + +
+ {/* Gauge background arc */} + + {/* Background arc */} + + {/* Colored arc */} + + {/* Needle */} + + + + + +
+
+
+ {successRate}% +
+

+ {successRate >= 70 && "Excellent performance!"} + {successRate >= 40 && successRate < 70 && "Good progress!"} + {successRate < 40 && "Keep applying!"} +

+
+
+
+ ) +} diff --git a/frontend/components/profile/GitHubSection.tsx b/frontend/components/profile/GitHubSection.tsx index ca43cc4..ae76ff1 100644 --- a/frontend/components/profile/GitHubSection.tsx +++ b/frontend/components/profile/GitHubSection.tsx @@ -1,4 +1,8 @@ +"use client" + +import { useState } from "react" import GithubButton from "@/components/GithubButton" +import GitHubDeleteButton from "@/components/GitHubDeleteButton" interface GitHubSectionProps { hasGitHub: boolean @@ -8,11 +12,17 @@ interface GitHubSectionProps { } export default function GitHubSection({ hasGitHub, githubData, applicant, isOwner }: GitHubSectionProps) { + const [githubExists, setGithubExists] = useState(hasGitHub) + + const handleDelete = () => { + setGithubExists(false) + } + return (

GitHub Integration

- {hasGitHub ? ( + {githubExists ? (
{githubData ? ( <> @@ -99,13 +109,18 @@ export default function GitHubSection({ hasGitHub, githubData, applicant, isOwne /> + + {isOwner && } ) : (

Unable to fetch GitHub data.

)}
) : ( - isOwner && + isOwner && )}
) diff --git a/frontend/components/profile/LinkedInSection.tsx b/frontend/components/profile/LinkedInSection.tsx index 060f6ae..7aff96c 100644 --- a/frontend/components/profile/LinkedInSection.tsx +++ b/frontend/components/profile/LinkedInSection.tsx @@ -1,4 +1,8 @@ +"use client" + +import { useState } from "react" import LinkedinButton from "@/components/LinkedinButton" +import LinkedInDeleteButton from "@/components/LinkedInDeleteButton" interface LinkedInSectionProps { hasLinkedIn: boolean @@ -7,29 +11,38 @@ interface LinkedInSectionProps { } export default function LinkedInSection({ hasLinkedIn, applicant, isOwner }: LinkedInSectionProps) { + const [linkedInExists, setLinkedInExists] = useState(hasLinkedIn) + + const handleDelete = () => { + setLinkedInExists(false) + } + return (

LinkedIn Integration

- {hasLinkedIn ? ( - - View LinkedIn Profile - - - - + {linkedInExists ? ( + ) : ( - isOwner && + isOwner && )}
) diff --git a/frontend/components/profile/ProfileHeader.tsx b/frontend/components/profile/ProfileHeader.tsx index f92c237..c0b592f 100644 --- a/frontend/components/profile/ProfileHeader.tsx +++ b/frontend/components/profile/ProfileHeader.tsx @@ -11,6 +11,8 @@ interface ProfileHeaderProps { hasResume: boolean hasGitHub: boolean hasLinkedIn: boolean + isRecruiter?: boolean + recruiter?: any } export default function ProfileHeader({ @@ -21,6 +23,8 @@ export default function ProfileHeader({ hasResume, hasGitHub, hasLinkedIn, + isRecruiter = false, + recruiter, }: ProfileHeaderProps) { return (
@@ -47,8 +51,8 @@ export default function ProfileHeader({

{profileUser.email}

- {/* Social Links */} - {(hasResume || hasGitHub || hasLinkedIn) && ( + {/* Social Links - Only for applicants */} + {isApplicant && (hasResume || hasGitHub || hasLinkedIn) && (
{hasResume && ( - + GitHub @@ -108,8 +112,8 @@ export default function ProfileHeader({ ) : (

No "About" provided yet.

) - ) : profileUser.recruiter?.about ? ( -

{profileUser.recruiter.about}

+ ) : isRecruiter && recruiter?.about ? ( +

{recruiter.about}

) : (

No "About" information available.

)} @@ -117,7 +121,7 @@ export default function ProfileHeader({ {isOwner && ( )} diff --git a/frontend/components/profile/RecruiterCompanySection.tsx b/frontend/components/profile/RecruiterCompanySection.tsx new file mode 100644 index 0000000..353e009 --- /dev/null +++ b/frontend/components/profile/RecruiterCompanySection.tsx @@ -0,0 +1,77 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" + +interface RecruiterCompanySectionProps { + recruiter: any + profileUser: any + isOwner: boolean +} + +export default function RecruiterCompanySection({ recruiter, profileUser, isOwner }: RecruiterCompanySectionProps) { + return ( + + + Company Information + Details about the recruiting organization + + + {recruiter?.company ? ( +
+
+

Company Name

+

{recruiter.company.name}

+
+ {recruiter.company.industry && ( +
+

Industry

+ {recruiter.company.industry} +
+ )} + {recruiter.company.description && ( +
+

About Company

+

{recruiter.company.description}

+
+ )} +
+ ) : ( +
+

No company information available

+ {isOwner && ( +

Add your company details to attract candidates

+ )} +
+ )} + + {recruiter?.position && ( +
+

Position

+

{recruiter.position}

+
+ )} + + {recruiter?.website && ( +
+

Company Website

+ + {recruiter.website} + + + + +
+ )} +
+
+ ) +} diff --git a/frontend/components/profile/RecruiterContactSection.tsx b/frontend/components/profile/RecruiterContactSection.tsx new file mode 100644 index 0000000..3dea366 --- /dev/null +++ b/frontend/components/profile/RecruiterContactSection.tsx @@ -0,0 +1,36 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" + +interface RecruiterContactSectionProps { + recruiter: any + profileUser: any + isOwner: boolean +} + +export default function RecruiterContactSection({ recruiter, profileUser, isOwner }: RecruiterContactSectionProps) { + return ( + + + Contact Information + Get in touch with the recruiter + + + + + {recruiter?.about && ( +
+

About Recruiter

+

{recruiter.about}

+
+ )} +
+
+ ) +} diff --git a/frontend/components/profile/RecruiterListingsSection.tsx b/frontend/components/profile/RecruiterListingsSection.tsx new file mode 100644 index 0000000..a5b4059 --- /dev/null +++ b/frontend/components/profile/RecruiterListingsSection.tsx @@ -0,0 +1,138 @@ +"use client" + +import { useEffect, useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" + +interface Internship { + id: string + title: string + description: string + location: string + type: string + status: string + applicantsCount: number +} + +interface RecruiterListingsSectionProps { + recruiterId: string + isOwner: boolean +} + +export default function RecruiterListingsSection({ recruiterId, isOwner }: RecruiterListingsSectionProps) { + const [internships, setInternships] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchInternships = async () => { + try { + const response = await fetch(`/api/recruiter/internships?recruiterId=${recruiterId}`) + if (response.ok) { + const data = await response.json() + setInternships(data.internships) + } + } catch (error) { + console.error("[v0] Error fetching internships:", error) + } finally { + setLoading(false) + } + } + + fetchInternships() + }, [recruiterId]) + + if (loading) { + return ( + + + Active Internship Listings + Current opportunities from this recruiter + + +
+ {[...Array(3)].map((_, i) => ( +
+ ))} +
+ + + ) + } + + return ( + + + Active Internship Listings + Current opportunities from this recruiter + + + {internships.length === 0 ? ( +
+

No active listings at the moment

+ {isOwner && ( +

+ Create your first internship listing to attract candidates +

+ )} +
+ ) : ( +
+ {internships.map((internship) => ( +
+
+

{internship.title}

+ {internship.status} +
+

{internship.description}

+
+ + + + + + {internship.location} + + + + + + {internship.type} + + + + + + {internship.applicantsCount} applicants + +
+
+ ))} +
+ )} +
+
+ ) +} diff --git a/frontend/package.json b/frontend/package.json index 744f84b..68a911e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,6 @@ "start": "next start" }, "dependencies": { - "@auth/core": "^0.41.0", "@hookform/resolvers": "^3.10.0", "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^6.16.2",