import React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Briefcase, Building2, MapPin, Clock, ChevronDown, Zap, Cpu, ExternalLink, } from "lucide-react"; import { Card, Button, Badge, Spinner } from "@/components/common"; import { useJobStore } from "@/store/appStore"; import { jobsAPI } from "@/utils/api"; import { downloadFile } from "@/utils/helpers"; import type { Job } from "@/types"; import { JobFitAnalyzer } from "../sections/JobFitAnalyzer"; interface JobListPageProps { onBack?: () => void; onHomeClick?: () => void; onSettingsClick?: () => void; } const JobListPage: React.FC = ({ onBack, onHomeClick, onSettingsClick, }) => { const { jobs, setJobs, // Temporary selectedJobIds, toggleJobSelection, selectAllJobs, deselectAllJobs, isGenerating, setIsGenerating, batchProgress, setBatchProgress, addGeneratedCV, setNotification, tailoringMode, llmProvider, enableProfessionalSummary, includeCoverLetters, } = useJobStore(); const [expandedJobId, setExpandedJobId] = React.useState(null); const [generatingJobId, setGeneratingJobId] = React.useState( null ); const selectedCount = selectedJobIds.size; const allSelected = selectedCount === jobs.length && jobs.length > 0; const getLLMProviderLabel = (provider: string) => { const labels: Record = { ollama: "Ollama", groq: "Groq", grok: "Grok", openai: "OpenAI", }; return labels[provider] || provider; }; const handleGenerateSingleCV = async (job: Job) => { setGeneratingJobId(job.id); try { const response = await jobsAPI.generateCV(job.id); if (response.success) { // Temporary if statement below if (response.job) { const updatedJobs = jobs.map((j) => j.id === job.id ? { ...j, ...response.job } : j ); setJobs(updatedJobs as Job[]); } addGeneratedCV({ jobId: job.id, jobTitle: job.title, company: job.company, filename: response.filename, url: response.url, coverLetterFilename: response.cover_letter_filename, coverLetterUrl: response.cover_letter_url, generatedAt: new Date().toISOString(), status: "success", }); const hasCoverLetter = !!response.cover_letter_filename; setNotification({ type: "success", message: hasCoverLetter ? `CV + Cover Letter generated for ${job.company} - ${job.title}` : `CV generated for ${job.company} - ${job.title}`, }); // Download the file const blob = await jobsAPI.downloadFile(response.filename); downloadFile(blob, response.filename); if (response.cover_letter_filename) { const coverLetterBlob = await jobsAPI.downloadFile( response.cover_letter_filename ); downloadFile(coverLetterBlob, response.cover_letter_filename); } } } catch (error) { setNotification({ type: "error", message: "Failed to generate CV", }); } finally { setGeneratingJobId(null); } }; const handleGenerateAllCVs = async () => { if (selectedCount === 0) { setNotification({ type: "error", message: "Please select at least one job", }); return; } setIsGenerating(true); setBatchProgress({ total: selectedCount, completed: 0, failed: 0, isProcessing: true, }); try { const jobIds = Array.from(selectedJobIds); const response = await jobsAPI.generateAllCVs(jobIds); setBatchProgress({ total: selectedCount, completed: response.successful.length, failed: response.failed.length, isProcessing: false, }); response.successful.forEach((cv) => { addGeneratedCV(cv); }); setNotification({ type: "success", message: `Generated ${response.successful.length} CVs successfully`, }); // Download the ZIP file const blob = await jobsAPI.downloadFile(response.zip_filename); downloadFile(blob, response.zip_filename); } catch (error) { setNotification({ type: "error", message: "Batch generation failed", }); } finally { setIsGenerating(false); } }; if (jobs.length === 0) { return (

No Jobs Found

Search for jobs to get started with CV generation

); } // TODO: Add website link that opens up the company's own website to apply to instead of applying on linkedIn // BUG: The Selenium Scrapper data does not show up correctly sometims on the frontend return (
{/* Header */}
Tailored CVs
{tailoringMode === "api" && (
{getLLMProviderLabel(llmProvider || "groq")}
)}
Summary: {enableProfessionalSummary ? "On" : "Off"}
Cover Letters: {includeCoverLetters ? "On" : "Off"}
{onSettingsClick && ( )}

Found {jobs.length} Jobs

Select jobs to generate tailored CVs

{/* Selection Controls */}
{ if (allSelected) { deselectAllJobs(); } else { selectAllJobs(); } }} className="w-5 h-5 rounded border-slate-600 cursor-pointer" /> {selectedCount > 0 ? ( <> {selectedCount} job {selectedCount !== 1 ? "s" : ""} selected ) : ( "Select jobs to generate CVs" )}
{selectedCount > 0 && ( )}
{/* Jobs List */} {jobs.map((job, index) => { // --- SAFETIES ADDED HERE --- const matchedSkills = job.matched_skills || []; const matchedCategories = job.matched_categories || {}; const isRawData = matchedSkills.length === 0; console.log(job); return (
{/* ========================================== */} {/* TOP SECTION: Header, Meta, and Action Footer */} {/* ========================================== */}
toggleJobSelection(job.id)} className="w-5 h-5 rounded border-slate-600 cursor-pointer mt-1 shrink-0" />
{/* Top Row: Title, Company & Expand Chevron */}

{job.title}

{job.company}

setExpandedJobId( expandedJobId === job.id ? null : job.id ) } animate={{ rotate: expandedJobId === job.id ? 180 : 0, }} className="text-slate-400 hover:text-white transition-colors shrink-0 p-1" >
{/* Job Meta with Safeties for missing data */}
{job.location && (
{job.location}
)}
{job.posted_days_ago} days ago
{job.source}
{job.link && ( View on LinkedIn )}
{/* Skills Preview with Safeties */}
{matchedSkills.slice(0, 3).map((skill) => ( {skill} ))} {matchedSkills.length > 3 && ( +{matchedSkills.length - 3} more )} {/* Show a badge indicating this is fast/raw data */} {isRawData && ( Ready for AI CV Tailoring )}
{/* --- FULL-WIDTH ACTION FOOTER --- */}
{/* Left Side: Analyzer Pill & Skills Drawer */}
{/* Right Side: Generate CV Button */}
{/* ========================================== */} {/* BOTTOM SECTION: Collapsible Job Description */} {/* ========================================== */} {expandedJobId === job.id && ( {/* LinkedIn Link Button */} {(job.apply_url || job.link) && ( Visit Job Post Page )} {/* Skill Categories with Safeties */} {Object.keys(matchedCategories).length > 0 && (

Matched Skills

{Object.entries(matchedCategories).map( ([category, skills]) => (

{category}

{(skills as string[]).map((skill) => ( {skill} ))}
) )}
)} {/* Job Description Preview */}

Job Description

{job.description || "No description provided."}

)}
); })}
{/* Batch Progress */} {batchProgress.isProcessing && (

{batchProgress.completed} / {batchProgress.total} CVs Generated

{batchProgress.current && (

{batchProgress.current}

)}
)}
); }; export default JobListPage;