193 lines
10 KiB
JavaScript
193 lines
10 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useWorkerProfile, useUpdateWorkerProfile, useCertifications } from '@/lib/krowHooks';
|
|
import { recalcProfilePatch } from '@/lib/krowScore';
|
|
import { base44 } from '@/api/base44Client';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { toast } from 'react-hot-toast';
|
|
import { Camera, Loader2, RefreshCw, X, Sparkles, ArrowRight } from 'lucide-react';
|
|
import KrowIdentityCard from '@/components/krow/KrowIdentityCard';
|
|
import CareerPassport from '@/components/krow/talent/CareerPassport';
|
|
|
|
const AVAIL_OPTIONS = ['Weekdays', 'Weekends', 'Evenings', 'Mornings', 'Overnight', 'On-Call'];
|
|
const DEFAULT_CERTS = ['Food Handler Card', 'ServSafe', 'ABC License', 'TIPS Certified', 'CPR/First Aid', 'Background Check Cleared'];
|
|
|
|
export default function KrowIdentity() {
|
|
const navigate = useNavigate();
|
|
const { data: profile, isLoading } = useWorkerProfile();
|
|
const update = useUpdateWorkerProfile();
|
|
const { data: certs = [] } = useCertifications();
|
|
const [form, setForm] = useState(null);
|
|
const [skillInput, setSkillInput] = useState('');
|
|
const [langInput, setLangInput] = useState('');
|
|
const [uploadingSelfie, setUploadingSelfie] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (profile && !form) {
|
|
setForm({
|
|
full_name: profile.full_name || '',
|
|
phone: profile.phone || '',
|
|
address: profile.address || '',
|
|
career_goals: profile.career_goals || '',
|
|
skills: profile.skills || [],
|
|
languages: profile.languages || [],
|
|
availability: profile.availability || [],
|
|
certifications: profile.certifications || [],
|
|
selfie_url: profile.selfie_url || '',
|
|
});
|
|
}
|
|
}, [profile, form]);
|
|
|
|
if (isLoading || !profile || !form) {
|
|
return <div className="flex justify-center py-20"><Loader2 className="w-6 h-6 animate-spin text-[#0A39DF]" /></div>;
|
|
}
|
|
|
|
const set = (k, v) => setForm({ ...form, [k]: v });
|
|
const certOptions = certs.length > 0 ? certs.map(c => c.name) : DEFAULT_CERTS;
|
|
|
|
const toggleCert = (c) => set('certifications', form.certifications.includes(c) ? form.certifications.filter(x => x !== c) : [...form.certifications, c]);
|
|
const toggleAvail = (a) => set('availability', form.availability.includes(a) ? form.availability.filter(a2 => a2 !== a) : [...form.availability, a]);
|
|
|
|
const handleSelfie = async (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
setUploadingSelfie(true);
|
|
try {
|
|
const { file_url } = await base44.integrations.Core.UploadFile({ file });
|
|
set('selfie_url', file_url);
|
|
} catch {}
|
|
setUploadingSelfie(false);
|
|
e.target.value = '';
|
|
};
|
|
|
|
const save = async () => {
|
|
if (!form.full_name.trim()) { toast.error('Your name is required.'); return; }
|
|
if (!form.selfie_url) { toast.error('Add a selfie to complete your identity.'); return; }
|
|
try {
|
|
const merged = { ...profile, ...form };
|
|
const patch = { ...form, ...recalcProfilePatch(merged) };
|
|
await update.mutateAsync({ id: profile.id, data: patch });
|
|
toast.success('KROW Identity created. Now let Owliver learn about you.');
|
|
navigate('/owliver');
|
|
} catch {
|
|
toast.error('Could not save identity.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto space-y-6">
|
|
{/* Living identity card — the experience, not the explanation */}
|
|
<KrowIdentityCard form={form} />
|
|
|
|
{/* Career Passport — verified credentials that make up the KROW Identity */}
|
|
<CareerPassport profile={profile} />
|
|
|
|
{/* Selfie + name */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6 space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
{form.selfie_url ? (
|
|
<img src={form.selfie_url} alt="Selfie" className="w-20 h-20 rounded-2xl object-cover border border-[#E5E7EB]" />
|
|
) : (
|
|
<div className="w-20 h-20 rounded-2xl bg-[#F3F4F6] flex items-center justify-center">
|
|
<Camera className="w-7 h-7 text-[#9CA3AF]" />
|
|
</div>
|
|
)}
|
|
<div className="flex-1">
|
|
<Label className="text-[13px] text-[#374151]">Your face *</Label>
|
|
<p className="text-[11px] text-[#6B7280] mb-1.5">A quick selfie so employers recognize you. No resume, just you.</p>
|
|
{form.selfie_url ? (
|
|
<button type="button" onClick={() => set('selfie_url', '')} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#F3F4F6] hover:bg-[#E5E7EB] text-[#374151] text-[12px] font-medium">
|
|
<RefreshCw className="w-3.5 h-3.5" /> Retake
|
|
</button>
|
|
) : (
|
|
<label className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[12px] font-medium cursor-pointer">
|
|
{uploadingSelfie ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Camera className="w-3.5 h-3.5" />}
|
|
{uploadingSelfie ? 'Uploading...' : 'Take selfie'}
|
|
<input type="file" accept="image/*" capture="user" onChange={handleSelfie} disabled={uploadingSelfie} className="hidden" />
|
|
</label>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label className="text-[13px] text-[#374151]">Full Name *</Label>
|
|
<Input value={form.full_name} onChange={e => set('full_name', e.target.value)} className="mt-1.5" />
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div><Label className="text-[13px] text-[#374151]">Phone</Label><Input value={form.phone} onChange={e => set('phone', e.target.value)} className="mt-1.5" /></div>
|
|
<div><Label className="text-[13px] text-[#374151]">Location</Label><Input value={form.address} onChange={e => set('address', e.target.value)} placeholder="City, State" className="mt-1.5" /></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* About */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6 space-y-2">
|
|
<Label className="text-[13px] text-[#374151]">About you</Label>
|
|
<p className="text-[11px] text-[#6B7280]">One or two lines. What you do and what you're good at. This replaces the resume summary.</p>
|
|
<textarea value={form.career_goals} onChange={e => set('career_goals', e.target.value)} className="w-full min-h-[80px] p-3 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0A39DF] focus:outline-none resize-none" placeholder="e.g. Line cook with 4 years in fast-casual kitchens. Fast, clean, reliable." />
|
|
</div>
|
|
|
|
{/* Skills */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6">
|
|
<Label className="text-[13px] text-[#374151]">Top Skills</Label>
|
|
<div className="flex gap-2 mt-1.5">
|
|
<Input value={skillInput} onChange={e => setSkillInput(e.target.value)} placeholder="Add a skill..." className="flex-1"
|
|
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); if (skillInput.trim()) { set('skills', [...form.skills, skillInput.trim()]); setSkillInput(''); } } }} />
|
|
<Button type="button" variant="outline" onClick={() => { if (skillInput.trim()) { set('skills', [...form.skills, skillInput.trim()]); setSkillInput(''); } }}>Add</Button>
|
|
</div>
|
|
<Chips items={form.skills} onRemove={(i) => set('skills', form.skills.filter((_, idx) => idx !== i))} />
|
|
</div>
|
|
|
|
{/* Languages */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6">
|
|
<Label className="text-[13px] text-[#374151]">Languages</Label>
|
|
<div className="flex gap-2 mt-1.5">
|
|
<Input value={langInput} onChange={e => setLangInput(e.target.value)} placeholder="e.g. English, Spanish" className="flex-1"
|
|
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); if (langInput.trim()) { set('languages', [...form.languages, langInput.trim()]); setLangInput(''); } } }} />
|
|
<Button type="button" variant="outline" onClick={() => { if (langInput.trim()) { set('languages', [...form.languages, langInput.trim()]); setLangInput(''); } }}>Add</Button>
|
|
</div>
|
|
<Chips items={form.languages} onRemove={(i) => set('languages', form.languages.filter((_, idx) => idx !== i))} tone="blue" />
|
|
</div>
|
|
|
|
{/* Availability */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6">
|
|
<Label className="text-[13px] text-[#374151]">Availability</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1.5">
|
|
{AVAIL_OPTIONS.map(a => (
|
|
<button key={a} type="button" onClick={() => toggleAvail(a)} className={`px-3 py-1.5 rounded-full text-[12px] font-medium border transition-all ${form.availability.includes(a) ? 'bg-[#059669] border-[#059669] text-white' : 'border-[#E5E7EB] text-[#6B7280] hover:border-[#D1D5DB]'}`}>{a}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Certifications */}
|
|
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-5 sm:p-6">
|
|
<Label className="text-[13px] text-[#374151]">Certifications</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1.5">
|
|
{certOptions.map(c => (
|
|
<button key={c} type="button" onClick={() => toggleCert(c)} className={`px-3 py-1.5 rounded-full text-[12px] font-medium border transition-all ${form.certifications.includes(c) ? 'bg-[#0A39DF] border-[#0A39DF] text-white' : 'border-[#E5E7EB] text-[#6B7280] hover:border-[#D1D5DB]'}`}>{c}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Button onClick={save} disabled={update.isPending} className="w-full h-11 text-[14px] bg-[#0A39DF] hover:bg-[#082EB4]">
|
|
{update.isPending ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Sparkles className="w-4 h-4 mr-2" />}
|
|
Create Identity <ArrowRight className="w-4 h-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Chips({ items, onRemove, tone = 'gray' }) {
|
|
if (!items?.length) return null;
|
|
const cls = tone === 'blue' ? 'bg-[#EFF6FF] text-[#1E40AF] border-[#DBEAFE]' : 'bg-[#F3F4F6] text-[#374151]';
|
|
return (
|
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
{items.map((s, i) => (
|
|
<span key={i} className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-[12px] border ${cls}`}>
|
|
{s}
|
|
<button type="button" onClick={() => onRemove(i)} className="text-[#9CA3AF] hover:text-[#EF4444]"><X className="w-3 h-3" /></button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
);
|
|
} |