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
+191
View File
@@ -0,0 +1,191 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
import { useTheme } from '../../context/ThemeContext';
import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
import UrgencyPillSelector from './UrgencyPillSelector';
// ---------------------------------------------------------------------------
// Shared field primitives (local to Job section for now)
// ---------------------------------------------------------------------------
function FieldLabel({ children }) {
return (
<label className="block text-[11px] uppercase tracking-widest font-bold text-zinc-400 dark:text-zinc-500 mb-1.5">
{children}
</label>
);
}
function LeadSelect({ label, value, onChange, options, placeholder }) {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div>
<FieldLabel>{label}</FieldLabel>
<div className="relative">
<select
value={value}
onChange={e => onChange(e.target.value)}
className={`w-full appearance-none rounded-xl px-4 py-3 pr-10 text-sm font-medium cursor-pointer
outline-none transition-all duration-150
${isDark
? 'bg-zinc-800/60 border border-zinc-700 text-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20'
: 'bg-white border border-zinc-200 text-zinc-800 focus:border-blue-400 focus:ring-2 focus:ring-blue-100'
}
${!value ? (isDark ? 'text-zinc-500' : 'text-zinc-400') : ''}
`}
>
<option value="" disabled>{placeholder || 'Select…'}</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<ChevronDown
size={14}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none"
/>
</div>
</div>
);
}
function LeadTextarea({ label, value, onChange, placeholder, rows = 3 }) {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div>
<FieldLabel>{label}</FieldLabel>
<textarea
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
rows={rows}
className={`w-full rounded-xl px-4 py-3 text-sm font-medium resize-none
outline-none transition-all duration-150
${isDark
? 'bg-zinc-800/60 border border-zinc-700 text-white placeholder-zinc-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20'
: 'bg-white border border-zinc-200 text-zinc-800 placeholder-zinc-400 focus:border-blue-400 focus:ring-2 focus:ring-blue-100'
}
`}
/>
</div>
);
}
// Shared animation config for collapsing rows
const collapseTransition = {
type: 'spring',
stiffness: 300,
damping: 32,
opacity: { duration: 0.15 },
};
// ---------------------------------------------------------------------------
// Job Details Section
// ---------------------------------------------------------------------------
export default function LeadJobSection({ formData, updateField, isQuickCapture }) {
return (
// No gap/space-y — each block controls its own top padding to avoid
// phantom gaps from collapsed motion.divs
<div>
{/* --- Lead Source — always visible --- */}
<LeadSelect
label="Lead Source"
value={formData.leadSource}
onChange={v => updateField('leadSource', v)}
options={LEAD_FORM_OPTIONS.leadSources}
placeholder="How did you find this lead?"
/>
{/* --- Lead Type — Full Form only --- */}
<AnimatePresence initial={false}>
{!isQuickCapture && (
<motion.div
key="leadType"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={collapseTransition}
className="overflow-hidden"
>
<div className="pt-4">
<LeadSelect
label="Lead Type"
value={formData.leadType}
onChange={v => updateField('leadType', v)}
options={LEAD_FORM_OPTIONS.leadTypes}
placeholder="Residential, Commercial…"
/>
</div>
</motion.div>
)}
</AnimatePresence>
{/* --- Work Type + Trade Type — Full Form only --- */}
<AnimatePresence initial={false}>
{!isQuickCapture && (
<motion.div
key="workTrade"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={collapseTransition}
className="overflow-hidden"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4">
<LeadSelect
label="Work Type"
value={formData.workType}
onChange={v => updateField('workType', v)}
options={LEAD_FORM_OPTIONS.workTypes}
placeholder="Roof Replacement, Repair…"
/>
<LeadSelect
label="Trade Type"
value={formData.tradeType}
onChange={v => updateField('tradeType', v)}
options={LEAD_FORM_OPTIONS.tradeTypes}
placeholder="Roofing, Gutter, Siding…"
/>
</div>
</motion.div>
)}
</AnimatePresence>
{/* --- Urgency — always visible --- */}
<div className="pt-4">
<FieldLabel>Urgency</FieldLabel>
<UrgencyPillSelector
value={formData.urgency}
onChange={v => updateField('urgency', v)}
/>
</div>
{/* --- Notes — Full Form only --- */}
<AnimatePresence initial={false}>
{!isQuickCapture && (
<motion.div
key="notes"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={collapseTransition}
className="overflow-hidden"
>
<div className="pt-4">
<LeadTextarea
label="Notes"
value={formData.notes}
onChange={v => updateField('notes', v)}
placeholder="First impression, visible damage, special circumstances…"
rows={3}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}