feat(leads): Lead Creation Phase 2 — Job Details section with live fields

- UrgencyPillSelector: 3-pill selector (Standard/High/Emergency) with Framer Motion layoutId sliding background, spring physics, color transition on text
- LeadJobSection: full Job Details fields — Lead Source (always), Lead Type / Work Type / Trade Type (Full Form only, each row animates in/out with spring height collapse), Urgency pill (always), Notes textarea (Full Form only)
- Full Form fields use overflow-hidden + height:auto spring animation — no phantom gaps since spacing is managed via pt-4 inside each animated block
- CreateLeadPage: formData state tracking all sections (scaffold for Phase 3-6 fields), updateField handler, sectionCompletion map, GSAP progress bar now driven by real field completion percentage, Job Details section wired up with live data
This commit is contained in:
Satyam
2026-03-13 16:13:45 +05:30
parent 8bb321e1f7
commit a2e4fc9039
3 changed files with 345 additions and 27 deletions
+104 -27
View File
@@ -8,6 +8,7 @@ import {
ChevronLeft, Zap, FileText, Save,
} from 'lucide-react';
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
import LeadJobSection from '../components/leads/LeadJobSection';
import gsap from 'gsap';
// ---------------------------------------------------------------------------
@@ -73,6 +74,41 @@ const sectionVariants = {
},
};
// ---------------------------------------------------------------------------
// Initial form state
// ---------------------------------------------------------------------------
const INITIAL_FORM = {
// Job Details
leadSource: '',
leadType: '',
workType: '',
tradeType: '',
urgency: 'standard',
notes: '',
// Contact (Phase 3)
firstName: '',
lastName: '',
phones: [],
emails: [],
// Property (Phase 4)
address: '',
city: '',
state: 'TX',
zip: '',
propertyType: '',
// Insurance (Phase 5)
insuranceCompany: '',
claimNumber: '',
claimStatus: '',
adjusterName: '',
adjusterPhone: '',
policyNumber: '',
// Assignment (Phase 6)
assignedTo: '',
priority: 'medium',
followUpDate: '',
};
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
@@ -90,19 +126,45 @@ export default function CreateLeadPage() {
insurance: false,
assignment: false,
});
const [formData, setFormData] = useState(INITIAL_FORM);
const progressBarRef = useRef(null);
// Animate progress bar on mount (Phase 1: stays at 0)
// Derived: which fields must be filled for this mode
const requiredFields = isQuickCapture
? ['leadSource']
: ['leadSource', 'leadType', 'workType'];
const filledCount = requiredFields.filter(f => formData[f]).length;
const progress = requiredFields.length > 0
? Math.round((filledCount / requiredFields.length) * 100)
: 0;
// GSAP animates the progress bar whenever progress changes
useEffect(() => {
if (progressBarRef.current) {
gsap.fromTo(
progressBarRef.current,
{ scaleX: 0, transformOrigin: 'left' },
{ scaleX: 0, duration: 0.6, ease: 'power2.out' }
);
gsap.to(progressBarRef.current, {
width: `${progress}%`,
duration: 0.45,
ease: 'power2.out',
});
}
}, []);
}, [progress]);
// Section completion map — grows as phases add fields
const sectionCompletion = {
contact: false, // Phase 3
property: false, // Phase 4
job: isQuickCapture
? !!formData.leadSource
: !!(formData.leadSource && formData.leadType && formData.workType),
insurance: false, // Phase 5
assignment: false, // Phase 6
};
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const toggleSection = (id) => {
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
@@ -115,21 +177,47 @@ export default function CreateLeadPage() {
const modeSwitchTo = (quick) => {
if (quick === isQuickCapture) return;
setIsQuickCapture(quick);
// Close insurance + assignment when switching to quick capture
if (quick) {
setOpenSections(prev => ({ ...prev, insurance: false, assignment: false }));
}
};
// Render the content for a given section (placeholder until its phase is built)
const renderSectionContent = (section) => {
if (section.id === 'job') {
return (
<LeadJobSection
formData={formData}
updateField={updateField}
isQuickCapture={isQuickCapture}
/>
);
}
// Placeholder for sections not yet built
return (
<div className={`rounded-xl border-2 border-dashed px-6 py-8 flex flex-col items-center justify-center gap-2 text-center
${isDark ? 'border-zinc-700' : 'border-zinc-200'}
`}>
<section.icon
size={28}
style={{ color: section.accentColor, opacity: 0.35 }}
/>
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-400">
{section.description}
</p>
</div>
);
};
return (
<div className={`min-h-screen transition-colors duration-300
${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}
`}>
{/* Progress bar — thin accent line at top */}
{/* --- Progress bar --- */}
<div className={`h-[3px] w-full ${isDark ? 'bg-zinc-800' : 'bg-zinc-200'}`}>
<div
ref={progressBarRef}
className="h-full bg-blue-500 transition-all duration-500 ease-out"
className="h-full bg-blue-500"
style={{ width: '0%' }}
/>
</div>
@@ -141,7 +229,6 @@ export default function CreateLeadPage() {
{/* --- Page Header --- */}
<div className="mb-7">
{/* Back breadcrumb */}
<button
type="button"
onClick={() => navigate(-1)}
@@ -164,7 +251,7 @@ export default function CreateLeadPage() {
</p>
</div>
{/* Quick Capture / Full Form toggle */}
{/* Quick / Full Form toggle */}
<div className={`flex items-center p-1 rounded-xl gap-0.5 shrink-0
${isDark ? 'bg-zinc-800' : 'bg-zinc-200/80'}
`}>
@@ -195,7 +282,7 @@ export default function CreateLeadPage() {
</div>
</div>
{/* Mode indicator pills */}
{/* Section indicator pills */}
<div className="flex items-center gap-2 mt-4 flex-wrap">
{SECTIONS.map(s => (
<div
@@ -243,19 +330,9 @@ export default function CreateLeadPage() {
accentColor={section.accentColor}
isOpen={openSections[section.id]}
onToggle={() => toggleSection(section.id)}
isComplete={sectionCompletion[section.id]}
>
{/* Placeholder — fields added in Phase 2+ */}
<div className={`rounded-xl border-2 border-dashed px-6 py-8 flex flex-col items-center justify-center gap-2 text-center
${isDark ? 'border-zinc-700' : 'border-zinc-200'}
`}>
<section.icon
size={28}
style={{ color: section.accentColor, opacity: 0.4 }}
/>
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-400">
{section.description}
</p>
</div>
{renderSectionContent(section)}
</LeadSectionWrapper>
</motion.div>
))}
@@ -274,7 +351,7 @@ export default function CreateLeadPage() {
}
`}>
<div className="flex items-center gap-3 max-w-3xl mx-auto">
{/* Mode toggle — compact repeat for mobile */}
{/* Compact mode toggle */}
<div className={`flex items-center p-0.5 rounded-lg gap-0.5 shrink-0
${isDark ? 'bg-zinc-800' : 'bg-zinc-100'}
`}>
@@ -302,7 +379,7 @@ export default function CreateLeadPage() {
</button>
</div>
{/* Save button */}
{/* Save */}
<button
type="button"
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold text-sm uppercase tracking-widest text-white transition-all active:scale-[0.97]"