d3e2944fa3
- Replace all native <select> with LeadCustomSelect (ReactDOM.createPortal into body, getBoundingClientRect positioning, e.composedPath() scroll detection) - Add LeadCombobox: fuzzy-search state picker (searches abbr + full name, portal, onMouseDown) - Convert Phone/MapPin icons from absolute-positioned to flex-wrapper siblings of bg-transparent input - Fix dark mode invisible text: all inputs converted from isDark JS conditionals to dark: variants - Fix urgency Standard pill visibility: container bg-zinc-900, selected bg-zinc-700 ring-1 shadow-lg - Fix City/State/ZIP mobile overlap: flex -> grid-cols-2 (mobile) / grid-cols-[1fr_5.5rem_6rem] (sm+) - Fix urgency section hidden behind mobile sticky footer: pb-28 -> pb-44 md:pb-10 - Replace all compact type pickers (phone/email) with LeadCustomSelect compact mode - Update README.md with full lead creation architecture docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
6.5 KiB
React
157 lines
6.5 KiB
React
import React from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
|
|
import UrgencyPillSelector from './UrgencyPillSelector';
|
|
import LeadCustomSelect from './LeadCustomSelect';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared field primitives (local to Job section)
|
|
// ---------------------------------------------------------------------------
|
|
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 LeadTextarea({ label, value, onChange, placeholder, rows = 3 }) {
|
|
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
|
|
bg-white dark:bg-zinc-800/60
|
|
border border-zinc-200 dark:border-zinc-700
|
|
text-zinc-800 dark:text-white
|
|
placeholder-zinc-400 dark:placeholder-zinc-500
|
|
focus:border-blue-400 dark:focus:border-blue-500
|
|
focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-500/20"
|
|
/>
|
|
</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 (
|
|
<div>
|
|
{/* --- Lead Source — always visible --- */}
|
|
<LeadCustomSelect
|
|
label="Lead Source"
|
|
value={formData.leadSource}
|
|
onChange={v => updateField('leadSource', v)}
|
|
options={LEAD_FORM_OPTIONS.leadSources}
|
|
placeholder="How did you find this lead?"
|
|
accent="blue"
|
|
/>
|
|
|
|
{/* --- 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">
|
|
<LeadCustomSelect
|
|
label="Lead Type"
|
|
value={formData.leadType}
|
|
onChange={v => updateField('leadType', v)}
|
|
options={LEAD_FORM_OPTIONS.leadTypes}
|
|
placeholder="Residential, Commercial…"
|
|
accent="blue"
|
|
/>
|
|
</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">
|
|
<LeadCustomSelect
|
|
label="Work Type"
|
|
value={formData.workType}
|
|
onChange={v => updateField('workType', v)}
|
|
options={LEAD_FORM_OPTIONS.workTypes}
|
|
placeholder="Roof Replacement, Repair…"
|
|
accent="blue"
|
|
/>
|
|
<LeadCustomSelect
|
|
label="Trade Type"
|
|
value={formData.tradeType}
|
|
onChange={v => updateField('tradeType', v)}
|
|
options={LEAD_FORM_OPTIONS.tradeTypes}
|
|
placeholder="Roofing, Gutter, Siding…"
|
|
accent="blue"
|
|
/>
|
|
</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>
|
|
);
|
|
}
|