Files
krow_talent_app/src/pages/Apply.tsx

169 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ChevronRight, Clock, Mic, Gauge, ArrowLeft } from 'lucide-react';
import { useJobPostings, useCreateApplication, useCourses } from '@/lib/krowHooks';
import { YourSkillFit } from '@/components/krow/MatchedCandidates';
import { verifiedLevels } from '@/lib/skillGraph';
import ApplyIdentityCard from '@/components/krow/ApplyIdentityCard';
import AIInterviewModal from '@/components/krow/AIInterviewModal';
import { useWorkerProfile } from '@/lib/krowHooks';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
export default function Apply() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const jobId = searchParams.get('job');
const { data: postings = [], isLoading } = useJobPostings();
const createApplication = useCreateApplication();
const { data: profile } = useWorkerProfile();
const { data: courses = [] } = useCourses();
const [selectedJob, setSelectedJob] = useState(null);
const [showForm, setShowForm] = useState(false);
const [interviewApp, setInterviewApp] = useState(null);
const [starting, setStarting] = useState(false);
const openPositions = postings.filter(p => p.status === 'active');
// Deep-link from invitation: auto-select job if ?job=<id> is in URL
useEffect(() => {
if (jobId && !isLoading) {
const job = openPositions.find(p => p.id === jobId);
if (job && job.id !== selectedJob?.id) {
setSelectedJob(job);
setShowForm(true);
}
}
}, [jobId, isLoading, openPositions, selectedJob]);
// Auto-build a JobApplication from the worker's KROW Identity — no manual form.
const handleStartInterview = async () => {
if (!selectedJob || !profile) return;
setStarting(true);
try {
const formData = {
applicant_name: profile.full_name || '',
email: profile.email || '',
phone: profile.phone || '',
years_experience: profile.experience_years || 0,
english_level: 'basic',
certifications: profile.certifications || [],
availability: profile.availability || [],
skills: profile.skills || [],
companies_worked: (profile.experience || []).map(e => e.company).filter(Boolean),
professional_summary: profile.personality || '',
cover_letter: '',
resume_url: profile.resume_url || '',
selfie_url: profile.selfie_url || '',
certification_files: [],
};
const app = await createApplication.mutateAsync({
...formData,
job_posting_id: selectedJob.id,
job_title: selectedJob.title,
status: 'applied',
});
toast.success('Application created from your KROW Identity!');
setShowForm(false);
setInterviewApp({ ...app, job_posting_id: selectedJob.id });
} catch {
toast.error('Failed to start application');
} finally {
setStarting(false);
}
};
return (
<div className="max-w-3xl mx-auto space-y-6 px-4 sm:px-0">
{/* Mobile back link */}
{showForm && (
<button onClick={() => { setShowForm(false); setSelectedJob(null); navigate('/apply', { replace: true }); }} className="text-[13px] text-[#6B7280] hover:text-[#111827] font-medium flex items-center gap-1">
<ArrowLeft className="w-3.5 h-3.5" /> {t('apply.backToPositions')}
</button>
)}
{/* Hero */}
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-6 sm:p-8 text-center">
<h1 className="text-[28px] font-bold text-[#111827] mb-2">{t('apply.skipTheQuizJustTalk')}</h1>
<p className="text-[14px] text-[#6B7280] max-w-lg mx-auto mb-5">
{t('apply.krowReplacesLongFormsAndTests')}
</p>
<div className="flex items-center justify-center gap-2 flex-wrap">
{[
{ icon: Clock, label: '5 minutes' },
{ icon: Mic, label: 'Voice or Text' },
{ icon: Gauge, label: 'Instant Score' },
].map(b => (
<span key={b.label} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-[#E5E7EB] text-[12px] text-[#374151] font-medium">
<b.icon className="w-3.5 h-3.5 text-[#6B7280]" />
{b.label}
</span>
))}
</div>
<button onClick={() => navigate('/owliver')} className="mt-5 inline-flex items-center gap-2 bg-[#0838E0] hover:bg-[#062BAF] text-white text-[13px] font-semibold px-5 py-2.5 rounded-full transition-all">
{t('apply.createYourKrowIdentity')} <ChevronRight className="w-4 h-4" />
</button>
</div>
{showForm && selectedJob ? (
/* KROW Identity preview — auto-applies from profile, then AI interview */
<div className="space-y-4">
<ApplyIdentityCard
profile={profile}
job={selectedJob}
submitting={starting || createApplication.isPending}
onStart={handleStartInterview}
onBuildIdentity={() => navigate((profile?.profile_completion || 0) < 40 ? '/owliver' : '/identity')}
/>
{/* The candidate's own fit against this role — their skills, this
position's requirements, and nothing about anybody else. The
recruiter's ranked comparison of every candidate is the same
arithmetic and stays on the admin position page. */}
<YourSkillFit job={selectedJob} held={verifiedLevels(courses, profile || {})} />
<button onClick={() => { setShowForm(false); setSelectedJob(null); navigate('/apply', { replace: true }); }} className="text-[13px] text-[#6B7280] hover:text-[#111827] font-medium flex items-center gap-1">
<ArrowLeft className="w-3.5 h-3.5" /> {t('apply.backToPositions')}
</button>
</div>
) : (
/* Position List */
<div>
<p className="text-[13px] font-medium text-[#374151] mb-3">{t('apply.chooseAPosition')}</p>
<div className="space-y-2">
{isLoading && [...Array(4)].map((_, i) => <div key={i} className="h-16 bg-white border border-[#E5E7EB] rounded-xl animate-pulse" />)}
{openPositions.map(job => (
<button
key={job.id}
onClick={() => { setSelectedJob(job); setShowForm(true); }}
className="w-full flex items-center gap-3 sm:gap-4 bg-white border border-[#E5E7EB] hover:border-[#D1D5DB] hover:shadow-sm rounded-xl p-4 transition-all text-left"
>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-[#111827] text-[14px] sm:text-[15px] truncate">{job.title}</h3>
<p className="text-[12px] text-[#6B7280] truncate">{job.role_category} • {job.location}</p>
<span className="sm:hidden text-[12px] font-bold text-[#111827]">${job.pay_range_min}–${job.pay_range_max}/hr</span>
</div>
<span className="hidden sm:block font-bold text-[#111827] text-[14px] whitespace-nowrap">${job.pay_range_min}–${job.pay_range_max}/hr</span>
<ChevronRight className="w-5 h-5 text-[#9CA3AF] shrink-0" />
</button>
))}
{!isLoading && openPositions.length === 0 && (
<div className="text-center py-12 text-[14px] text-[#6B7280]">{t('apply.noOpenPositionsRightNowCheck')}</div>
)}
</div>
</div>
)}
{/* AI Interview after applying */}
{interviewApp && (
<AIInterviewModal
open={true}
onClose={() => { setInterviewApp(null); navigate('/'); }}
application={interviewApp}
job={selectedJob}
conductor="Owliver"
/>
)}
</div>
);
}