From a2e4fc903996401256142d42aaa6a15aaeac0d6f Mon Sep 17 00:00:00 2001
From: Satyam <95536056+Satyam-Rastogi@users.noreply.github.com>
Date: Fri, 13 Mar 2026 16:13:45 +0530
Subject: [PATCH] =?UTF-8?q?feat(leads):=20Lead=20Creation=20Phase=202=20?=
=?UTF-8?q?=E2=80=94=20Job=20Details=20section=20with=20live=20fields?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
src/components/leads/LeadJobSection.jsx | 191 +++++++++++++++++++
src/components/leads/UrgencyPillSelector.jsx | 50 +++++
src/pages/CreateLeadPage.jsx | 131 ++++++++++---
3 files changed, 345 insertions(+), 27 deletions(-)
create mode 100644 src/components/leads/LeadJobSection.jsx
create mode 100644 src/components/leads/UrgencyPillSelector.jsx
diff --git a/src/components/leads/LeadJobSection.jsx b/src/components/leads/LeadJobSection.jsx
new file mode 100644
index 0000000..0333842
--- /dev/null
+++ b/src/components/leads/LeadJobSection.jsx
@@ -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 (
+
+ {children}
+
+ );
+}
+
+function LeadSelect({ label, value, onChange, options, placeholder }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ return (
+
+
{label}
+
+ 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') : ''}
+ `}
+ >
+ {placeholder || 'Select…'}
+ {options.map(opt => (
+ {opt}
+ ))}
+
+
+
+
+ );
+}
+
+function LeadTextarea({ label, value, onChange, placeholder, rows = 3 }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ return (
+
+ {label}
+
+ );
+}
+
+// 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
+
+ {/* --- Lead Source — always visible --- */}
+
updateField('leadSource', v)}
+ options={LEAD_FORM_OPTIONS.leadSources}
+ placeholder="How did you find this lead?"
+ />
+
+ {/* --- Lead Type — Full Form only --- */}
+
+ {!isQuickCapture && (
+
+
+ updateField('leadType', v)}
+ options={LEAD_FORM_OPTIONS.leadTypes}
+ placeholder="Residential, Commercial…"
+ />
+
+
+ )}
+
+
+ {/* --- Work Type + Trade Type — Full Form only --- */}
+
+ {!isQuickCapture && (
+
+
+ updateField('workType', v)}
+ options={LEAD_FORM_OPTIONS.workTypes}
+ placeholder="Roof Replacement, Repair…"
+ />
+ updateField('tradeType', v)}
+ options={LEAD_FORM_OPTIONS.tradeTypes}
+ placeholder="Roofing, Gutter, Siding…"
+ />
+
+
+ )}
+
+
+ {/* --- Urgency — always visible --- */}
+
+ Urgency
+ updateField('urgency', v)}
+ />
+
+
+ {/* --- Notes — Full Form only --- */}
+
+ {!isQuickCapture && (
+
+
+ updateField('notes', v)}
+ placeholder="First impression, visible damage, special circumstances…"
+ rows={3}
+ />
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/leads/UrgencyPillSelector.jsx b/src/components/leads/UrgencyPillSelector.jsx
new file mode 100644
index 0000000..652c9ae
--- /dev/null
+++ b/src/components/leads/UrgencyPillSelector.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import { useTheme } from '../../context/ThemeContext';
+
+const OPTIONS = [
+ { value: 'standard', label: 'Standard', color: '#6B7280' },
+ { value: 'high', label: 'High', color: '#F59E0B' },
+ { value: 'emergency', label: 'Emergency', color: '#EF4444' },
+];
+
+export default function UrgencyPillSelector({ value, onChange }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ return (
+
+ {OPTIONS.map(opt => {
+ const isSelected = value === opt.value;
+ return (
+ onChange(opt.value)}
+ className="relative flex-1 flex items-center justify-center py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-colors duration-150"
+ >
+ {/* Sliding background — Framer Motion layoutId handles the move */}
+ {isSelected && (
+
+ )}
+
+ {opt.label}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/pages/CreateLeadPage.jsx b/src/pages/CreateLeadPage.jsx
index 5787b4e..ac79544 100644
--- a/src/pages/CreateLeadPage.jsx
+++ b/src/pages/CreateLeadPage.jsx
@@ -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 (
+
+ );
+ }
+ // Placeholder for sections not yet built
+ return (
+
+
+
+ {section.description}
+
+
+ );
+ };
+
return (
- {/* Progress bar — thin accent line at top */}
+ {/* --- Progress bar --- */}
@@ -141,7 +229,6 @@ export default function CreateLeadPage() {
{/* --- Page Header --- */}
- {/* Back breadcrumb */}
navigate(-1)}
@@ -164,7 +251,7 @@ export default function CreateLeadPage() {
- {/* Quick Capture / Full Form toggle */}
+ {/* Quick / Full Form toggle */}
@@ -195,7 +282,7 @@ export default function CreateLeadPage() {
- {/* Mode indicator pills */}
+ {/* Section indicator pills */}
{SECTIONS.map(s => (
toggleSection(section.id)}
+ isComplete={sectionCompletion[section.id]}
>
- {/* Placeholder — fields added in Phase 2+ */}
-
-
-
- {section.description}
-
-
+ {renderSectionContent(section)}
))}
@@ -274,7 +351,7 @@ export default function CreateLeadPage() {
}
`}>
- {/* Mode toggle — compact repeat for mobile */}
+ {/* Compact mode toggle */}
@@ -302,7 +379,7 @@ export default function CreateLeadPage() {
- {/* Save button */}
+ {/* Save */}