From 18cd7d1e2708f6bf8958be086a8f571ab6da7afe Mon Sep 17 00:00:00 2001 From: kautilyadevaraj Date: Fri, 20 Jun 2025 00:47:52 +0530 Subject: [PATCH] added candidate UI --- hire-ai/app/(candidate)/header.tsx | 5 +- hire-ai/app/(candidate)/home/page.tsx | 335 +++- hire-ai/app/(candidate)/jobs/loading.tsx | 3 + hire-ai/app/(candidate)/jobs/page.tsx | 433 +++++ hire-ai/app/(candidate)/layout.tsx | 23 +- hire-ai/app/(candidate)/loading.tsx | 3 + .../app/(recruiter)/resume-parser/page.tsx | 1429 ++++++++++------- hire-ai/app/api/resume/delete/route.ts | 51 +- hire-ai/app/api/resume/upload/route.ts | 260 +-- hire-ai/app/globals.css | 2 +- hire-ai/app/jobs/job-card.tsx | 0 hire-ai/app/jobs/page.tsx | 13 - hire-ai/app/settings/page.tsx | 600 ++----- hire-ai/components/candidate-sidebar.tsx | 98 ++ hire-ai/components/date-picker.tsx | 73 + hire-ai/components/header.tsx | 5 +- .../settings/candidate-profile-section.tsx | 672 ++++++++ .../components/settings/form-validation.ts | 113 ++ .../settings/notifications-section.tsx | 138 ++ .../settings/preferences-section.tsx | 153 ++ .../components/settings/privacy-section.tsx | 151 ++ .../components/settings/profile-section.tsx | 206 +++ hire-ai/types/resume.ts | 76 +- hire-ai/types/user.ts | 57 + 24 files changed, 3677 insertions(+), 1222 deletions(-) create mode 100644 hire-ai/app/(candidate)/jobs/loading.tsx create mode 100644 hire-ai/app/(candidate)/jobs/page.tsx create mode 100644 hire-ai/app/(candidate)/loading.tsx delete mode 100644 hire-ai/app/jobs/job-card.tsx delete mode 100644 hire-ai/app/jobs/page.tsx create mode 100644 hire-ai/components/candidate-sidebar.tsx create mode 100644 hire-ai/components/date-picker.tsx create mode 100644 hire-ai/components/settings/candidate-profile-section.tsx create mode 100644 hire-ai/components/settings/form-validation.ts create mode 100644 hire-ai/components/settings/notifications-section.tsx create mode 100644 hire-ai/components/settings/preferences-section.tsx create mode 100644 hire-ai/components/settings/privacy-section.tsx create mode 100644 hire-ai/components/settings/profile-section.tsx create mode 100644 hire-ai/types/user.ts diff --git a/hire-ai/app/(candidate)/header.tsx b/hire-ai/app/(candidate)/header.tsx index cd758e2..000f4d3 100644 --- a/hire-ai/app/(candidate)/header.tsx +++ b/hire-ai/app/(candidate)/header.tsx @@ -77,7 +77,10 @@ export function Header() { Profile - + Settings diff --git a/hire-ai/app/(candidate)/home/page.tsx b/hire-ai/app/(candidate)/home/page.tsx index c79358d..364094d 100644 --- a/hire-ai/app/(candidate)/home/page.tsx +++ b/hire-ai/app/(candidate)/home/page.tsx @@ -1,10 +1,335 @@ -export default function CandidateHomePage() { +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Progress } from "@/components/ui/progress"; +import { + Search, + Briefcase, + MessageSquare, + TrendingUp, + Clock, + CheckCircle, + Eye, + Star, +} from "lucide-react"; +import Link from "next/link"; + +const stats = [ + { + title: "Profile Views", + value: "127", + change: "+23%", + icon: Eye, + color: "text-blue-600", + }, + { + title: "Applications", + value: "8", + change: "+2", + icon: Briefcase, + color: "text-green-600", + }, + { + title: "Messages", + value: "5", + change: "+3", + icon: MessageSquare, + color: "text-purple-600", + }, + { + title: "Profile Score", + value: "85%", + change: "+5%", + icon: Star, + color: "text-orange-600", + }, +]; + +const recentActivity = [ + { + action: "Profile viewed by TechCorp", + time: "2 hours ago", + type: "info", + }, + { + action: "Application submitted to AI Innovations", + time: "1 day ago", + type: "success", + }, + { + action: "New message from recruiter", + time: "2 days ago", + type: "warning", + }, + { + action: "Interview scheduled with DataTech", + time: "3 days ago", + type: "success", + }, +]; + +const recommendedJobs = [ + { + title: "Senior ML Engineer", + company: "TechCorp Inc.", + location: "San Francisco, CA", + salary: "$180k - $220k", + match: 95, + skills: ["PyTorch", "LangChain", "RAG"], + posted: "2 days ago", + }, + { + title: "AI Research Scientist", + company: "AI Innovations", + location: "New York, NY", + salary: "$200k - $250k", + match: 92, + skills: ["TensorFlow", "NLP", "Research"], + posted: "1 week ago", + }, + { + title: "GenAI Engineer", + company: "Future Labs", + location: "Austin, TX", + salary: "$160k - $190k", + match: 89, + skills: ["OpenAI API", "Vector DBs", "LLMs"], + posted: "3 days ago", + }, +]; + +export default function CandidateDashboard() { return ( -
-
-

Welcome to Hire AI

-

Placeholder

+
+
+

+ Welcome back, Sarah! +

+

+ Here's your career progress and new opportunities. +

+ + {/* Stats Grid */} +
+ {stats.map((stat) => ( + + + + {stat.title} + + + + +
+ {stat.value} +
+

+ + {stat.change} + {" "} + from last month +

+
+
+ ))} +
+ +
+ {/* Profile Completion */} + + + Profile Completion + + Complete your profile to get better matches + + + +
+
+ Overall Progress + 85% +
+ +
+
+
+ Basic Info + +
+
+ Work Experience + +
+
+ Skills & Certifications + +
+
+ Portfolio + +
+
+ +
+
+ + {/* Recent Activity */} + + + Recent Activity + + Your latest career activities + + + +
+ {recentActivity.map((activity, index) => ( +
+
+ {activity.type === "success" && ( + + )} + {activity.type === "info" && ( + + )} + {activity.type === "warning" && ( + + )} +
+
+

+ {activity.action} +

+

+ {activity.time} +

+
+
+ ))} +
+
+
+ + {/* Recommended Jobs */} + + + Recommended Jobs + + Jobs matching your profile + + + +
+ {recommendedJobs.slice(0, 2).map((job, index) => ( +
+
+
+

+ {job.title} +

+

+ {job.company} +

+
+ + {job.match}% + +
+

+ {job.location} • {job.salary} +

+
+ {job.skills.slice(0, 2).map((skill) => ( + + {skill} + + ))} +
+
+ ))} +
+ +
+
+
+ + {/* Quick Actions */} + + + Quick Actions + + Common tasks to advance your career + + + +
+ + + + +
+
+
); } diff --git a/hire-ai/app/(candidate)/jobs/loading.tsx b/hire-ai/app/(candidate)/jobs/loading.tsx new file mode 100644 index 0000000..709afa3 --- /dev/null +++ b/hire-ai/app/(candidate)/jobs/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null; +} diff --git a/hire-ai/app/(candidate)/jobs/page.tsx b/hire-ai/app/(candidate)/jobs/page.tsx new file mode 100644 index 0000000..5f497c4 --- /dev/null +++ b/hire-ai/app/(candidate)/jobs/page.tsx @@ -0,0 +1,433 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Slider } from "@/components/ui/slider"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Search, + Filter, + MapPin, + Clock, + Star, + ExternalLink, + Bookmark, + Heart, +} from "lucide-react"; + +const mockJobs = [ + { + id: 1, + title: "Senior ML Engineer", + company: "TechCorp Inc.", + location: "San Francisco, CA", + salary: "$180k - $220k", + type: "Full-time", + remote: true, + match: 95, + skills: ["PyTorch", "LangChain", "RAG", "Python", "AWS"], + description: + "Join our AI team to build next-generation machine learning systems...", + posted: "2 days ago", + applicants: 23, + logo: "/placeholder.svg?height=40&width=40", + }, + { + id: 2, + title: "AI Research Scientist", + company: "AI Innovations", + location: "New York, NY", + salary: "$200k - $250k", + type: "Full-time", + remote: false, + match: 92, + skills: ["TensorFlow", "NLP", "Computer Vision", "Research", "PhD"], + description: + "Lead cutting-edge research in artificial intelligence and machine learning...", + posted: "1 week ago", + applicants: 45, + logo: "/placeholder.svg?height=40&width=40", + }, + { + id: 3, + title: "GenAI Engineer", + company: "Future Labs", + location: "Austin, TX", + salary: "$160k - $190k", + type: "Full-time", + remote: true, + match: 89, + skills: ["OpenAI API", "Vector DBs", "LLMs", "React", "Node.js"], + description: "Build innovative generative AI applications and tools...", + posted: "3 days ago", + applicants: 18, + logo: "/placeholder.svg?height=40&width=40", + }, + { + id: 4, + title: "Machine Learning Engineer", + company: "DataTech Solutions", + location: "Seattle, WA", + salary: "$170k - $200k", + type: "Full-time", + remote: true, + match: 87, + skills: ["Scikit-learn", "MLOps", "Docker", "Kubernetes", "GCP"], + description: + "Design and implement ML pipelines for production systems...", + posted: "5 days ago", + applicants: 31, + logo: "/placeholder.svg?height=40&width=40", + }, +]; + +export default function JobSearchPage() { + const [searchQuery, setSearchQuery] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [salaryRange, setSalaryRange] = useState([100, 300]); + const [savedJobs, setSavedJobs] = useState([]); + + const toggleSaveJob = (jobId: number) => { + setSavedJobs((prev) => + prev.includes(jobId) + ? prev.filter((id) => id !== jobId) + : [...prev, jobId], + ); + }; + + return ( +
+
+

+ Job Search +

+

+ Discover AI and ML opportunities that match your skills and + interests. +

+
+ + {/* Search Interface */} + + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 pr-20 h-12 text-lg" + /> +
+ +
+
+
+
+
+ +
+ {/* Filters Sidebar */} + {showFilters && ( + + + Filters + + +
+ +
+ {[ + "Full-time", + "Part-time", + "Contract", + "Internship", + ].map((type) => ( +
+ + +
+ ))} +
+
+ +
+ + +
+ +
+ + +
+ ${salaryRange[0]}k + ${salaryRange[1]}k +
+
+ +
+ +
+ {[ + "Entry Level", + "Mid Level", + "Senior Level", + "Lead/Principal", + ].map((level) => ( +
+ + +
+ ))} +
+
+ +
+ +
+ {[ + "Python", + "PyTorch", + "TensorFlow", + "LangChain", + "RAG", + "NLP", + "Computer Vision", + "MLOps", + ].map((skill) => ( +
+ + +
+ ))} +
+
+
+
+ )} + + {/* Results */} +
+
+
+

+ Found {mockJobs.length} jobs +

+
+
+ + +
+
+ + {mockJobs.map((job) => ( + + +
+
+
+ {job.company} +
+
+
+

+ {job.title} +

+

+ {job.company} +

+
+
+
+ + {job.location} + {job.remote && ( + + Remote + + )} +
+
+ + {job.posted} +
+
+ + {job.applicants} applicants +
+
+

+ {job.description} +

+
+ {job.skills.map((skill) => ( + + {skill} + + ))} +
+
+
+
+ + {job.match}% match + +

+ {job.salary} +

+

+ {job.type} +

+
+ + +
+
+
+
+
+ ))} + + {/* Pagination */} +
+ + + + + +
+
+
+
+ ); +} diff --git a/hire-ai/app/(candidate)/layout.tsx b/hire-ai/app/(candidate)/layout.tsx index 9225cd1..fa0b066 100644 --- a/hire-ai/app/(candidate)/layout.tsx +++ b/hire-ai/app/(candidate)/layout.tsx @@ -3,10 +3,8 @@ import { jwtDecode, JwtPayload } from "jwt-decode"; import { redirect } from "next/navigation"; import { Header } from "./header"; import { getUserRole } from "@/lib/utils"; - -interface CustomJwtPayload extends JwtPayload { - user_role: string; -} +import { SidebarProvider } from "@/components/ui/sidebar"; +import { CandidateSidebar } from "@/components/candidate-sidebar"; export default async function CandidateLayout({ children, @@ -25,10 +23,19 @@ export default async function CandidateLayout({ } return ( -
-
-
-
{children}
+
+
+ +
+ +
+
+
+ {children} +
+
+
+
); diff --git a/hire-ai/app/(candidate)/loading.tsx b/hire-ai/app/(candidate)/loading.tsx new file mode 100644 index 0000000..709afa3 --- /dev/null +++ b/hire-ai/app/(candidate)/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null; +} diff --git a/hire-ai/app/(recruiter)/resume-parser/page.tsx b/hire-ai/app/(recruiter)/resume-parser/page.tsx index 138da3c..3bed681 100644 --- a/hire-ai/app/(recruiter)/resume-parser/page.tsx +++ b/hire-ai/app/(recruiter)/resume-parser/page.tsx @@ -1,609 +1,854 @@ "use client"; -import React, { useState, useCallback } from "react" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Progress } from "@/components/ui/progress" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Upload, FileText, CheckCircle, Clock, AlertCircle, X, Download, Zap, Trash2 } from "lucide-react" -import { UploadedResume, UploadResponse } from "@/types/resume" -import { useToast } from "@/hooks/use-toast" +import React, { useState, useCallback } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Upload, + FileText, + CheckCircle, + Clock, + AlertCircle, + X, + Download, + Zap, + Trash2, +} from "lucide-react"; +import { UploadedResume, UploadResponse } from "@/types/resume"; +import { useToast } from "@/hooks/use-toast"; export default function ResumeParserPage() { - const [dragActive, setDragActive] = useState(false) - const [uploadedResumes, setUploadedResumes] = useState([]) - const [selectedResume, setSelectedResume] = useState(null) - const { toast } = useToast() - - const handleDrag = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - if (e.type === "dragenter" || e.type === "dragover") { - setDragActive(true) - } else if (e.type === "dragleave") { - setDragActive(false) - } - }, []) - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setDragActive(false) - - const files = Array.from(e.dataTransfer.files) - files.forEach(file => uploadFile(file)) - }, []) - - const handleFileSelect = useCallback((e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []) - files.forEach(file => uploadFile(file)) - }, []) - - const uploadFile = async (file: File) => { - // Validate file type - if (!file.type.includes('pdf') && !file.type.includes('doc')) { - toast({ - title: "Invalid file type", - description: "Please upload PDF or DOC files only.", - variant: "destructive" - }) - return - } - - // Validate file size (10MB) - if (file.size > 10 * 1024 * 1024) { - toast({ - title: "File too large", - description: "File size must be less than 10MB.", - variant: "destructive" - }) - return - } - - // Create temporary resume object - const tempId = Date.now().toString() - const tempResume: UploadedResume = { - id: tempId, - filename: file.name, - originalName: file.name, - size: file.size, - uploadTime: new Date().toISOString(), - extractedText: "", - status: "uploading" - } - - setUploadedResumes(prev => [...prev, tempResume]) - - try { - // Update status to processing - setUploadedResumes(prev => - prev.map(resume => - resume.id === tempId - ? { ...resume, status: "processing" as const } - : resume - ) - ) - - const formData = new FormData() - formData.append('file', file) - - const response = await fetch('/api/resume/upload', { - method: 'POST', - body: formData - }) - - const result: UploadResponse = await response.json() - - if (result.success) { - // Update with successful result - const updatedResume: UploadedResume = { - id: tempId, - filename: result.filename, - originalName: result.originalName, - size: result.size, - uploadTime: result.uploadTime, - extractedText: result.extractedText, - structuredData: result.structuredData, - status: "completed" + const [dragActive, setDragActive] = useState(false); + const [uploadedResumes, setUploadedResumes] = useState( + [], + ); + const [selectedResume, setSelectedResume] = useState( + null, + ); + const { toast } = useToast(); + + const handleDrag = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + const files = Array.from(e.dataTransfer.files); + files.forEach((file) => uploadFile(file)); + }, []); + + const handleFileSelect = useCallback( + (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + files.forEach((file) => uploadFile(file)); + }, + [], + ); + + const uploadFile = async (file: File) => { + // Validate file type + if (!file.type.includes("pdf") && !file.type.includes("doc")) { + toast({ + title: "Invalid file type", + description: "Please upload PDF or DOC files only.", + variant: "destructive", + }); + return; + } + + // Validate file size (10MB) + if (file.size > 10 * 1024 * 1024) { + toast({ + title: "File too large", + description: "File size must be less than 10MB.", + variant: "destructive", + }); + return; } - setUploadedResumes(prev => - prev.map(resume => - resume.id === tempId ? updatedResume : resume - ) - ) - - // Auto-select the uploaded resume - setSelectedResume(updatedResume) - - toast({ - title: "Upload successful", - description: `Successfully extracted text from ${file.name}`, - }) - } else { - throw new Error(result.error || 'Upload failed') - } - } catch (error) { - console.error('Upload error:', error) - - // Update with error status - setUploadedResumes(prev => - prev.map(resume => - resume.id === tempId - ? { - ...resume, - status: "failed" as const, - error: error instanceof Error ? error.message : 'Upload failed' - } - : resume - ) - ) - - toast({ - title: "Upload failed", - description: error instanceof Error ? error.message : 'Failed to upload file', - variant: "destructive" - }) - } - } - - const removeResume = async (id: string) => { - const resume = uploadedResumes.find(r => r.id === id) - if (!resume) return - - // Show confirmation dialog - const confirmed = window.confirm( - `Are you sure you want to delete "${resume.originalName}"? This will permanently remove the file from the server.` - ) - - if (!confirmed) return - - try { - // Delete file from server if it has a filename (was successfully uploaded) - if (resume.filename && resume.status === 'completed') { - const response = await fetch(`/api/resume/delete?filename=${encodeURIComponent(resume.filename)}`, { - method: 'DELETE' - }) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Failed to delete file') + // Create temporary resume object + const tempId = Date.now().toString(); + const tempResume: UploadedResume = { + id: tempId, + filename: file.name, + originalName: file.name, + size: file.size, + uploadTime: new Date().toISOString(), + extractedText: "", + status: "uploading", + }; + + setUploadedResumes((prev) => [...prev, tempResume]); + + try { + // Update status to processing + setUploadedResumes((prev) => + prev.map((resume) => + resume.id === tempId + ? { ...resume, status: "processing" as const } + : resume, + ), + ); + + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("/api/resume/upload", { + method: "POST", + body: formData, + }); + + const result: UploadResponse = await response.json(); + + if (result.success) { + // Update with successful result + const updatedResume: UploadedResume = { + id: tempId, + filename: result.filename, + originalName: result.originalName, + size: result.size, + uploadTime: result.uploadTime, + extractedText: result.extractedText, + structuredData: result.structuredData, + status: "completed", + }; + + setUploadedResumes((prev) => + prev.map((resume) => + resume.id === tempId ? updatedResume : resume, + ), + ); + + // Auto-select the uploaded resume + setSelectedResume(updatedResume); + + toast({ + title: "Upload successful", + description: `Successfully extracted text from ${file.name}`, + }); + } else { + throw new Error(result.error || "Upload failed"); + } + } catch (error) { + console.error("Upload error:", error); + + // Update with error status + setUploadedResumes((prev) => + prev.map((resume) => + resume.id === tempId + ? { + ...resume, + status: "failed" as const, + error: + error instanceof Error + ? error.message + : "Upload failed", + } + : resume, + ), + ); + + toast({ + title: "Upload failed", + description: + error instanceof Error + ? error.message + : "Failed to upload file", + variant: "destructive", + }); } - } - - // Remove from UI - setUploadedResumes(prev => prev.filter(resume => resume.id !== id)) - if (selectedResume?.id === id) { - setSelectedResume(null) - } - - toast({ - title: "File deleted", - description: `Successfully deleted ${resume.originalName}`, - }) - } catch (error) { - console.error('Delete error:', error) - toast({ - title: "Delete failed", - description: error instanceof Error ? error.message : 'Failed to delete file', - variant: "destructive" - }) - } - } - - const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes' - const k = 1024 - const sizes = ['Bytes', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] - } - - const formatTimeAgo = (dateString: string) => { - const date = new Date(dateString) - const now = new Date() - const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60)) - - if (diffInMinutes < 1) return 'Just now' - if (diffInMinutes < 60) return `${diffInMinutes} minutes ago` - if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago` - return `${Math.floor(diffInMinutes / 1440)} days ago` - } - - return ( -
-
-

Resume Parser

-

Upload resumes to automatically extract and structure candidate information using AI.

-
- -
- {/* Upload Section */} -
- - - - - Upload Resumes - - - Drag and drop PDF or Word documents, or click to browse - - - -
document.getElementById('file-input')?.click()} - > - -

Drop resumes here

-

- Supports PDF, DOC, DOCX files up to 10MB + }; + + const removeResume = async (id: string) => { + const resume = uploadedResumes.find((r) => r.id === id); + if (!resume) return; + + // Show confirmation dialog + const confirmed = window.confirm( + `Are you sure you want to delete "${resume.originalName}"? This will permanently remove the file from the server.`, + ); + + if (!confirmed) return; + + try { + // Delete file from server if it has a filename (was successfully uploaded) + if (resume.filename && resume.status === "completed") { + const response = await fetch( + `/api/resume/delete?filename=${encodeURIComponent(resume.filename)}`, + { + method: "DELETE", + }, + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to delete file"); + } + } + + // Remove from UI + setUploadedResumes((prev) => + prev.filter((resume) => resume.id !== id), + ); + if (selectedResume?.id === id) { + setSelectedResume(null); + } + + toast({ + title: "File deleted", + description: `Successfully deleted ${resume.originalName}`, + }); + } catch (error) { + console.error("Delete error:", error); + toast({ + title: "Delete failed", + description: + error instanceof Error + ? error.message + : "Failed to delete file", + variant: "destructive", + }); + } + }; + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + const formatTimeAgo = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffInMinutes = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60), + ); + + if (diffInMinutes < 1) return "Just now"; + if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`; + if (diffInMinutes < 1440) + return `${Math.floor(diffInMinutes / 60)} hours ago`; + return `${Math.floor(diffInMinutes / 1440)} days ago`; + }; + + return ( +

+
+

+ Resume Parser +

+

+ Upload resumes to automatically extract and structure + candidate information using AI.

- - -
- - - - {/* Processing Queue */} - - - - - Processing Queue - {uploadedResumes.length > 0 && ( - - {uploadedResumes.length} - - )} - - - Uploaded files and their processing status - - - - {uploadedResumes.length === 0 ? ( -
- -

No files uploaded yet

-

Upload a resume to get started

-
- ) : ( -
- {uploadedResumes.map((resume, index) => ( -
resume.status === "completed" && setSelectedResume(resume)} - style={{ - animationDelay: `${(index + 3) * 100}ms`, - animation: `elegant-fade-in 500ms var(--ease-out-cubic) forwards`, - opacity: 0 - }} +
+ +
+ {/* Upload Section */} +
+ -
-
- -
-

- {resume.originalName} -

-

- {formatFileSize(resume.size)} • {formatTimeAgo(resume.uploadTime)} -

-
-
-
- {resume.status === "completed" && ( - - - Completed - - )} - {resume.status === "processing" && ( - - - Processing - - )} - {resume.status === "uploading" && ( - - - Uploading - - )} - {resume.status === "failed" && ( - - - Failed - - )} - -
-
- {resume.status === "processing" && ( - - )} - {resume.status === "failed" && resume.error && ( -

- {resume.error} -

- )} -
- ))} + + + + Upload Resumes + + + Drag and drop PDF or Word documents, or click to + browse + + + +
+ document + .getElementById("file-input") + ?.click() + } + > + +

+ Drop resumes here +

+

+ Supports PDF, DOC, DOCX files up to 10MB +

+ + +
+
+ + + {/* Processing Queue */} + + + + + Processing Queue + {uploadedResumes.length > 0 && ( + + {uploadedResumes.length} + + )} + + + Uploaded files and their processing status + + + + {uploadedResumes.length === 0 ? ( +
+ +

+ No files uploaded yet +

+

+ Upload a resume to get started +

+
+ ) : ( +
+ {uploadedResumes.map((resume, index) => ( +
+ resume.status === "completed" && + setSelectedResume(resume) + } + style={{ + animationDelay: `${(index + 3) * 100}ms`, + animation: `elegant-fade-in 500ms var(--ease-out-cubic) forwards`, + opacity: 0, + }} + > +
+
+ +
+

+ { + resume.originalName + } +

+

+ {formatFileSize( + resume.size, + )}{" "} + •{" "} + {formatTimeAgo( + resume.uploadTime, + )} +

+
+
+
+ {resume.status === + "completed" && ( + + + Completed + + )} + {resume.status === + "processing" && ( + + + Processing + + )} + {resume.status === + "uploading" && ( + + + Uploading + + )} + {resume.status === + "failed" && ( + + + Failed + + )} + +
+
+ {resume.status === "processing" && ( + + )} + {resume.status === "failed" && + resume.error && ( +

+ {resume.error} +

+ )} +
+ ))} +
+ )} +
+
- )} - - -
- {/* Results Section */} -
- - - - - Parsing Results - - - {selectedResume - ? `Structured information extracted from ${selectedResume.originalName}` - : "Select a completed upload to view parsed resume data" - } - - - - {!selectedResume ? ( -
- -

No resume selected

-

- Upload and process a resume to view structured data -

-
- ) : selectedResume.status !== "completed" ? ( -
- -

Processing resume...

-

- AI is analyzing and structuring the data -

-
- ) : selectedResume.structuredData ? ( - <> - - - Overview - Skills - Experience - - - -
-
-
- -

- {selectedResume.structuredData.name || "Not specified"} -

-
-
- -

- {selectedResume.structuredData.email || "Not specified"} -

-
-
- -

- {selectedResume.structuredData.phone || "Not specified"} -

-
-
- -

- {selectedResume.structuredData.location || "Not specified"} -

-
-
- -

- {selectedResume.structuredData.title || "Not specified"} -

-
-
- -

- {selectedResume.structuredData.experience || "Not specified"} -

-
- {selectedResume.structuredData.summary && ( -
- -

- {selectedResume.structuredData.summary} -

-
- )} -
-
-
- - -
- - {selectedResume.structuredData.skills.length > 0 ? ( -
- {selectedResume.structuredData.skills.map((skill, index) => ( - - {skill} - - ))} -
- ) : ( -

No skills extracted

- )} -
-
- - -
- {selectedResume.structuredData.education.length > 0 && ( -
- -
- {selectedResume.structuredData.education.map((edu, index) => ( -
-

{edu.degree}

-

- {edu.school} {edu.year && `• ${edu.year}`} -

- {edu.gpa && ( -

GPA: {edu.gpa}

- )} - {edu.honors && ( -

{edu.honors}

- )} + {/* Results Section */} +
+ + + + + Parsing Results + + + {selectedResume + ? `Structured information extracted from ${selectedResume.originalName}` + : "Select a completed upload to view parsed resume data"} + + + + {!selectedResume ? ( +
+ +

+ No resume selected +

+

+ Upload and process a resume to view + structured data +

- ))} -
-
- )} - - {selectedResume.structuredData.workHistory.length > 0 && ( -
- -
- {selectedResume.structuredData.workHistory.map((job, index) => ( -
-

{job.position}

-

- {job.company} • {job.duration} -

-

{job.description}

- {job.achievements && job.achievements.length > 0 && ( -
-

Key Achievements:

-
    - {job.achievements.map((achievement, i) => ( -
  • - - {achievement} -
  • - ))} -
+ ) : selectedResume.status !== "completed" ? ( +
+ +

+ Processing resume... +

+

+ AI is analyzing and structuring the data +

+
+ ) : selectedResume.structuredData ? ( + <> + + + + Overview + + + Skills + + + Experience + + + + +
+
+
+ +

+ {selectedResume + .structuredData + .name || + "Not specified"} +

+
+
+ +

+ {selectedResume + .structuredData + .email || + "Not specified"} +

+
+
+ +

+ {selectedResume + .structuredData + .phone || + "Not specified"} +

+
+
+ +

+ {selectedResume + .structuredData + .location || + "Not specified"} +

+
+
+ +

+ {selectedResume + .structuredData + .title || + "Not specified"} +

+
+
+ +

+ {selectedResume + .structuredData + .experience || + "Not specified"} +

+
+ {selectedResume + .structuredData + .summary && ( +
+ +

+ { + selectedResume + .structuredData + .summary + } +

+
+ )} +
+
+
+ + +
+ + {selectedResume.structuredData + .skills.length > 0 ? ( +
+ {selectedResume.structuredData.skills.map( + (skill, index) => ( + + {skill} + + ), + )} +
+ ) : ( +

+ No skills extracted +

+ )} +
+
+ + +
+ {selectedResume.structuredData + .education.length > 0 && ( +
+ +
+ {selectedResume.structuredData.education.map( + ( + edu, + index, + ) => ( +
+

+ { + edu.degree + } +

+

+ { + edu.school + }{" "} + {edu.year && + `• ${edu.year}`} +

+ {edu.gpa && ( +

+ GPA:{" "} + { + edu.gpa + } +

+ )} + {edu.honors && ( +

+ { + edu.honors + } +

+ )} +
+ ), + )} +
+
+ )} + + {selectedResume.structuredData + .workHistory.length > 0 && ( +
+ +
+ {selectedResume.structuredData.workHistory.map( + ( + job, + index, + ) => ( +
+

+ { + job.position + } +

+

+ { + job.company + }{" "} + •{" "} + { + job.duration + } +

+

+ { + job.description + } +

+ {job.achievements && + job + .achievements + .length > + 0 && ( +
+

+ Key + Achievements: +

+
    + {job.achievements.map( + ( + achievement, + i, + ) => ( +
  • + + { + achievement + } +
  • + ), + )} +
+
+ )} +
+ ), + )} +
+
+ )} + + {selectedResume.structuredData + .education.length === 0 && + selectedResume + .structuredData + .workHistory.length === + 0 && ( +

+ No education or work + history extracted +

+ )} +
+
+
+ +
+ + +
- )} + + ) : ( +
+ +

+ Processing failed +

+

+ Unable to extract structured data from + this resume +

- ))} -
-
- )} - - {selectedResume.structuredData.education.length === 0 && selectedResume.structuredData.workHistory.length === 0 && ( -

No education or work history extracted

- )} -
- - - -
- - - -
- - ) : ( -
- -

Processing failed

-

- Unable to extract structured data from this resume -

+ )} + +
- )} - - +
-
-
- ) + ); } diff --git a/hire-ai/app/api/resume/delete/route.ts b/hire-ai/app/api/resume/delete/route.ts index 645ce54..448a362 100644 --- a/hire-ai/app/api/resume/delete/route.ts +++ b/hire-ai/app/api/resume/delete/route.ts @@ -1,27 +1,32 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from "next/server"; export async function DELETE(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const filename = searchParams.get('filename'); - - if (!filename) { - return NextResponse.json({ error: 'Filename is required' }, { status: 400 }); - } + try { + const { searchParams } = new URL(request.url); + const filename = searchParams.get("filename"); + + if (!filename) { + return NextResponse.json( + { error: "Filename is required" }, + { status: 400 }, + ); + } - // In serverless environments like Vercel, files are processed in memory only - // No actual file deletion is needed since files aren't saved to filesystem - // This endpoint exists for compatibility with the frontend delete functionality - - return NextResponse.json({ - success: true, - message: 'File reference removed successfully' - }); + // In serverless environments like Vercel, files are processed in memory only + // No actual file deletion is needed since files aren't saved to filesystem + // This endpoint exists for compatibility with the frontend delete functionality - } catch (error) { - console.error('Delete error:', error); - return NextResponse.json({ - error: 'Failed to remove file reference' - }, { status: 500 }); - } -} \ No newline at end of file + return NextResponse.json({ + success: true, + message: "File reference removed successfully", + }); + } catch (error) { + console.error("Delete error:", error); + return NextResponse.json( + { + error: "Failed to remove file reference", + }, + { status: 500 }, + ); + } +} diff --git a/hire-ai/app/api/resume/upload/route.ts b/hire-ai/app/api/resume/upload/route.ts index b1598c2..223c7fb 100644 --- a/hire-ai/app/api/resume/upload/route.ts +++ b/hire-ai/app/api/resume/upload/route.ts @@ -1,86 +1,99 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { GoogleGenerativeAI } from '@google/generative-ai'; -import { ResumeStructuredData } from '@/types/resume'; +import { NextRequest, NextResponse } from "next/server"; +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { ResumeStructuredData } from "@/types/resume"; // Verify environment variable if (!process.env.GEMINI_API_KEY) { - console.error('GEMINI_API_KEY environment variable is not set'); + console.error("GEMINI_API_KEY environment variable is not set"); } const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); export async function POST(request: NextRequest) { - try { - const formData = await request.formData(); - const file = formData.get('file') as File; - - if (!file) { - return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); - } + try { + const formData = await request.formData(); + const file = formData.get("file") as File; + + if (!file) { + return NextResponse.json( + { error: "No file uploaded" }, + { status: 400 }, + ); + } - // Validate file type - if (!file.type.includes('pdf') && !file.type.includes('doc')) { - return NextResponse.json({ - error: 'Invalid file type. Please upload PDF or DOC files.' - }, { status: 400 }); - } + // Validate file type + if (!file.type.includes("pdf") && !file.type.includes("doc")) { + return NextResponse.json( + { + error: "Invalid file type. Please upload PDF or DOC files.", + }, + { status: 400 }, + ); + } - // Validate file size (10MB limit) - if (file.size > 10 * 1024 * 1024) { - return NextResponse.json({ - error: 'File too large. Maximum size is 10MB.' - }, { status: 400 }); - } + // Validate file size (10MB limit) + if (file.size > 10 * 1024 * 1024) { + return NextResponse.json( + { + error: "File too large. Maximum size is 10MB.", + }, + { status: 400 }, + ); + } - // Generate unique filename for reference (no file saving in serverless) - const timestamp = Date.now(); - const filename = `${timestamp}_${file.name.replace(/\s+/g, '_')}`; + // Generate unique filename for reference (no file saving in serverless) + const timestamp = Date.now(); + const filename = `${timestamp}_${file.name.replace(/\s+/g, "_")}`; - // Convert file to buffer for processing (in memory only) - const bytes = await file.arrayBuffer(); - const buffer = Buffer.from(bytes); + // Convert file to buffer for processing (in memory only) + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); - // Extract text using Gemini API - let extractedText = ''; - try { - const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); - - // Convert file to base64 for Gemini API - const base64Data = buffer.toString('base64'); - const mimeType = file.type; + // Extract text using Gemini API + let extractedText = ""; + try { + const model = genAI.getGenerativeModel({ + model: "gemini-1.5-flash", + }); + + // Convert file to base64 for Gemini API + const base64Data = buffer.toString("base64"); + const mimeType = file.type; - const prompt = ` + const prompt = ` Extract all text content from this resume/CV document. Preserve the structure and formatting as much as possible. Include all sections like personal information, experience, education, skills, etc. Return only the extracted text content. `; - const result = await model.generateContent([ - prompt, - { - inlineData: { - data: base64Data, - mimeType: mimeType - } + const result = await model.generateContent([ + prompt, + { + inlineData: { + data: base64Data, + mimeType: mimeType, + }, + }, + ]); + + const response = await result.response; + extractedText = response.text(); + } catch (error) { + console.error("Error extracting text with Gemini:", error); + extractedText = + "Error: Could not extract text from the document. Please try again or upload a different file."; } - ]); - const response = await result.response; - extractedText = response.text(); - - } catch (error) { - console.error('Error extracting text with Gemini:', error); - extractedText = 'Error: Could not extract text from the document. Please try again or upload a different file.'; - } + // Analyze extracted text to get structured data + let structuredData: ResumeStructuredData | null = null; + if (extractedText && !extractedText.startsWith("Error:")) { + try { + const analyzeModel = genAI.getGenerativeModel({ + model: "gemini-1.5-flash", + }); - // Analyze extracted text to get structured data - let structuredData: ResumeStructuredData | null = null; - if (extractedText && !extractedText.startsWith('Error:')) { - try { - const analyzeModel = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); - - const analyzePrompt = ` + const analyzePrompt = ` Analyze the following resume text and extract structured information. Return ONLY a valid JSON object with the following structure: { @@ -123,59 +136,84 @@ export async function POST(request: NextRequest) { ${extractedText} `; - const analyzeResult = await analyzeModel.generateContent(analyzePrompt); - const analyzeResponse = await analyzeResult.response; - let aiResponseText = analyzeResponse.text(); - - // Clean up the response - remove any markdown formatting or extra text - aiResponseText = aiResponseText.trim(); - if (aiResponseText.startsWith('```json')) { - aiResponseText = aiResponseText.replace(/^```json\s*/, '').replace(/\s*```$/, ''); - } else if (aiResponseText.startsWith('```')) { - aiResponseText = aiResponseText.replace(/^```\s*/, '').replace(/\s*```$/, ''); + const analyzeResult = + await analyzeModel.generateContent(analyzePrompt); + const analyzeResponse = await analyzeResult.response; + let aiResponseText = analyzeResponse.text(); + + // Clean up the response - remove any markdown formatting or extra text + aiResponseText = aiResponseText.trim(); + if (aiResponseText.startsWith("```json")) { + aiResponseText = aiResponseText + .replace(/^```json\s*/, "") + .replace(/\s*```$/, ""); + } else if (aiResponseText.startsWith("```")) { + aiResponseText = aiResponseText + .replace(/^```\s*/, "") + .replace(/\s*```$/, ""); + } + + structuredData = JSON.parse( + aiResponseText, + ) as ResumeStructuredData; + + // Validate the structure + if (structuredData) { + const requiredFields = [ + "name", + "email", + "phone", + "location", + "title", + "experience", + "skills", + "education", + "workHistory", + ]; + const isValid = requiredFields.every( + (field) => field in structuredData!, + ); + + if (!isValid) { + throw new Error("Invalid structure returned from AI"); + } + } + } catch (error) { + console.error("Error analyzing text:", error); + // Continue without structured data + structuredData = null; + } } - structuredData = JSON.parse(aiResponseText) as ResumeStructuredData; - - // Validate the structure - if (structuredData) { - const requiredFields = ['name', 'email', 'phone', 'location', 'title', 'experience', 'skills', 'education', 'workHistory']; - const isValid = requiredFields.every(field => field in structuredData!); - - if (!isValid) { - throw new Error('Invalid structure returned from AI'); - } - } + return NextResponse.json({ + success: true, + filename: filename, // Reference only, no actual file saved + originalName: file.name, + size: file.size, + extractedText: extractedText, + structuredData: structuredData ?? undefined, + uploadTime: new Date().toISOString(), + }); + } catch (error) { + console.error("Upload error:", error); - } catch (error) { - console.error('Error analyzing text:', error); - // Continue without structured data - structuredData = null; - } - } + // More detailed error logging for debugging + if (error instanceof Error) { + console.error("Error message:", error.message); + console.error("Error stack:", error.stack); + } - return NextResponse.json({ - success: true, - filename: filename, // Reference only, no actual file saved - originalName: file.name, - size: file.size, - extractedText: extractedText, - structuredData: structuredData ?? undefined, - uploadTime: new Date().toISOString() - }); - - } catch (error) { - console.error('Upload error:', error); - - // More detailed error logging for debugging - if (error instanceof Error) { - console.error('Error message:', error.message); - console.error('Error stack:', error.stack); + return NextResponse.json( + { + error: "Internal server error", + details: + process.env.NODE_ENV === "development" + ? error instanceof Error + ? error.message + : "Unknown error" + : undefined, + }, + { status: 500 }, + ); } - - return NextResponse.json({ - error: 'Internal server error', - details: process.env.NODE_ENV === 'development' ? (error instanceof Error ? error.message : 'Unknown error') : undefined - }, { status: 500 }); - } -} \ No newline at end of file +} diff --git a/hire-ai/app/globals.css b/hire-ai/app/globals.css index 703c159..8c9a0a7 100644 --- a/hire-ai/app/globals.css +++ b/hire-ai/app/globals.css @@ -304,7 +304,7 @@ animation: none !important; transition: none !important; } - + [data-radix-dropdown-menu-content] { animation: none !important; transition: none !important; diff --git a/hire-ai/app/jobs/job-card.tsx b/hire-ai/app/jobs/job-card.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/hire-ai/app/jobs/page.tsx b/hire-ai/app/jobs/page.tsx deleted file mode 100644 index 97524b5..0000000 --- a/hire-ai/app/jobs/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// placeholder - -export default function JobsPage() { - return ( -
-

Jobs

-

- Explore available job opportunities and apply to join our team. -

- {/* Job listings will be rendered here */} -
- ); -} diff --git a/hire-ai/app/settings/page.tsx b/hire-ai/app/settings/page.tsx index f219c4b..44bd6c5 100644 --- a/hire-ai/app/settings/page.tsx +++ b/hire-ai/app/settings/page.tsx @@ -1,438 +1,180 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Separator } from "@/components/ui/separator"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { - User, - Bell, - Shield, - Palette, - Globe, - Mail, - Phone, - MapPin, - Briefcase, - Save, - Upload, - ArrowLeft, - LogOut -} from "lucide-react"; +import { ArrowLeft, LogOut } from "lucide-react"; import { useRouter } from "next/navigation"; -import { logout } from "@/app/login/actions"; - -export default function SettingsPage() { - const [emailNotifications, setEmailNotifications] = useState(true); - const [pushNotifications, setPushNotifications] = useState(true); - const [profileVisibility, setProfileVisibility] = useState(true); - const router = useRouter(); - - return ( -
-
-
- -
-

Settings

-

- Manage your account settings and preferences -

-
-
- -
- -
-
- - - - Profile - Notifications - Privacy - Preferences - - - - - - - - Profile Information - - - Update your personal information and profile details - - - -
- - - JD - -
- -

- JPG, PNG or GIF. Max size 2MB. -

-
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -