diff --git a/src/components/leads/EmailEntryRow.jsx b/src/components/leads/EmailEntryRow.jsx
new file mode 100644
index 0000000..d36a128
--- /dev/null
+++ b/src/components/leads/EmailEntryRow.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { Mail, Star, X } from 'lucide-react';
+import { useTheme } from '../../context/ThemeContext';
+import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
+
+export default function EmailEntryRow({ email, isOnly, onUpdate, onSetPrimary, onRemove }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ const inputBase = `w-full rounded-xl px-4 py-3 text-sm font-medium 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'
+ }`;
+
+ return (
+
+ {/* Email input */}
+
+
+ onUpdate('address', e.target.value)}
+ placeholder="name@example.com"
+ className={`${inputBase} pl-9`}
+ />
+
+
+ {/* Controls */}
+
+ {/* Type selector */}
+
+
onUpdate('type', e.target.value)}
+ className={`appearance-none rounded-xl px-3 py-3 pr-8 text-xs font-bold uppercase tracking-wide cursor-pointer outline-none transition-all duration-150
+ ${isDark
+ ? 'bg-zinc-800/60 border border-zinc-700 text-zinc-300 focus:border-blue-500'
+ : 'bg-white border border-zinc-200 text-zinc-600 focus:border-blue-400'
+ }
+ `}
+ >
+ {LEAD_FORM_OPTIONS.emailTypes.map(t => (
+ {t}
+ ))}
+
+
+
+
+
+
+ {/* Primary star */}
+
+
+
+
+ {/* Remove */}
+ {!isOnly && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/leads/LeadContactSection.jsx b/src/components/leads/LeadContactSection.jsx
new file mode 100644
index 0000000..17a1f27
--- /dev/null
+++ b/src/components/leads/LeadContactSection.jsx
@@ -0,0 +1,251 @@
+import React from 'react';
+import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
+import { PlusCircle } from 'lucide-react';
+import { useTheme } from '../../context/ThemeContext';
+import PhoneEntryRow from './PhoneEntryRow';
+import EmailEntryRow from './EmailEntryRow';
+
+// ---------------------------------------------------------------------------
+// Shared field primitives
+// ---------------------------------------------------------------------------
+function FieldLabel({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function LeadInput({ label, value, onChange, placeholder, type = 'text', autoComplete }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ return (
+
+ {label && {label} }
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ autoComplete={autoComplete}
+ className={`w-full rounded-xl px-4 py-3 text-sm font-medium
+ 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'
+ }
+ `}
+ />
+
+ );
+}
+
+// Row slide animation config
+const rowTransition = {
+ type: 'spring',
+ stiffness: 300,
+ damping: 32,
+ opacity: { duration: 0.15 },
+};
+
+// ---------------------------------------------------------------------------
+// Contact Section
+// ---------------------------------------------------------------------------
+export default function LeadContactSection({ formData, updateField, isQuickCapture }) {
+
+ // ---- Phone helpers ----
+ const addPhone = () => {
+ const next = {
+ id: Date.now().toString(),
+ number: '',
+ type: 'Mobile',
+ isPrimary: false,
+ };
+ updateField('phones', [...formData.phones, next]);
+ };
+
+ const removePhone = (id) => {
+ const filtered = formData.phones.filter(p => p.id !== id);
+ // Ensure at least one primary
+ if (filtered.length > 0 && !filtered.some(p => p.isPrimary)) {
+ filtered[0] = { ...filtered[0], isPrimary: true };
+ }
+ updateField('phones', filtered);
+ };
+
+ const updatePhone = (id, field, value) => {
+ updateField('phones', formData.phones.map(p =>
+ p.id === id ? { ...p, [field]: value } : p
+ ));
+ };
+
+ const setPrimaryPhone = (id) => {
+ updateField('phones', formData.phones.map(p => ({ ...p, isPrimary: p.id === id })));
+ };
+
+ // ---- Email helpers ----
+ const addEmail = () => {
+ const next = {
+ id: Date.now().toString(),
+ address: '',
+ type: 'Personal',
+ isPrimary: formData.emails.length === 0,
+ };
+ updateField('emails', [...formData.emails, next]);
+ };
+
+ const removeEmail = (id) => {
+ const filtered = formData.emails.filter(e => e.id !== id);
+ if (filtered.length > 0 && !filtered.some(e => e.isPrimary)) {
+ filtered[0] = { ...filtered[0], isPrimary: true };
+ }
+ updateField('emails', filtered);
+ };
+
+ const updateEmail = (id, field, value) => {
+ updateField('emails', formData.emails.map(e =>
+ e.id === id ? { ...e, [field]: value } : e
+ ));
+ };
+
+ const setPrimaryEmail = (id) => {
+ updateField('emails', formData.emails.map(e => ({ ...e, isPrimary: e.id === id })));
+ };
+
+ return (
+
+ {/* ---- Name row ---- */}
+
+ updateField('firstName', v)}
+ placeholder="John"
+ autoComplete="given-name"
+ />
+ updateField('lastName', v)}
+ placeholder="Smith"
+ autoComplete="family-name"
+ />
+
+
+ {/* ---- Phone(s) ---- */}
+
+
{isQuickCapture ? 'Phone' : 'Phone Numbers'}
+
+
+ {formData.phones.map((phone, index) => (
+
+ 0 ? 'pt-2' : ''}>
+
updatePhone(phone.id, field, value)}
+ onSetPrimary={() => setPrimaryPhone(phone.id)}
+ onRemove={() => removePhone(phone.id)}
+ />
+
+
+ ))}
+
+
+
+ {/* Add Phone button — Full Form only */}
+
+ {!isQuickCapture && (
+
+
+
+ Add Phone
+
+
+ )}
+
+
+
+ {/* ---- Email(s) — Full Form only ---- */}
+
+ {!isQuickCapture && (
+
+
+
Email Addresses
+
+
+
+ {formData.emails.map((email, index) => (
+
+ 0 ? 'pt-2' : ''}>
+ updateEmail(email.id, field, value)}
+ onSetPrimary={() => setPrimaryEmail(email.id)}
+ onRemove={() => removeEmail(email.id)}
+ />
+
+
+ ))}
+
+
+
+ {/* Empty state when no emails added yet */}
+ {formData.emails.length === 0 && (
+
+ No emails added yet.
+
+ )}
+
+
+
+ Add Email
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/leads/PhoneEntryRow.jsx b/src/components/leads/PhoneEntryRow.jsx
new file mode 100644
index 0000000..4b94752
--- /dev/null
+++ b/src/components/leads/PhoneEntryRow.jsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { Phone, Star, X } from 'lucide-react';
+import { useTheme } from '../../context/ThemeContext';
+import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
+
+const formatPhone = (raw) => {
+ const digits = raw.replace(/\D/g, '').slice(0, 10);
+ if (digits.length <= 3) return digits;
+ if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
+};
+
+export default function PhoneEntryRow({ phone, isOnly, showFullControls, onUpdate, onSetPrimary, onRemove }) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ const inputBase = `w-full rounded-xl px-4 py-3 text-sm font-medium 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'
+ }`;
+
+ return (
+
+ {/* Phone input */}
+
+
+
onUpdate('number', formatPhone(e.target.value))}
+ placeholder="(555) 000-0000"
+ className={`${inputBase} pl-9`}
+ />
+
+
+ {/* Controls row (type + star + remove) */}
+
+ {/* Type selector — shown in Full Form, hidden in Quick Capture */}
+ {showFullControls && (
+
+
onUpdate('type', e.target.value)}
+ className={`appearance-none rounded-xl px-3 py-3 pr-8 text-xs font-bold uppercase tracking-wide cursor-pointer outline-none transition-all duration-150
+ ${isDark
+ ? 'bg-zinc-800/60 border border-zinc-700 text-zinc-300 focus:border-blue-500'
+ : 'bg-white border border-zinc-200 text-zinc-600 focus:border-blue-400'
+ }
+ `}
+ >
+ {LEAD_FORM_OPTIONS.phoneTypes.map(t => (
+ {t}
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Primary star — Full Form only */}
+ {showFullControls && (
+
+
+
+ )}
+
+ {/* Remove — shown when more than one row */}
+ {!isOnly && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/CreateLeadPage.jsx b/src/pages/CreateLeadPage.jsx
index ac79544..b438bda 100644
--- a/src/pages/CreateLeadPage.jsx
+++ b/src/pages/CreateLeadPage.jsx
@@ -9,6 +9,7 @@ import {
} from 'lucide-react';
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
import LeadJobSection from '../components/leads/LeadJobSection';
+import LeadContactSection from '../components/leads/LeadContactSection';
import gsap from 'gsap';
// ---------------------------------------------------------------------------
@@ -85,10 +86,10 @@ const INITIAL_FORM = {
tradeType: '',
urgency: 'standard',
notes: '',
- // Contact (Phase 3)
+ // Contact
firstName: '',
lastName: '',
- phones: [],
+ phones: [{ id: '1', number: '', type: 'Mobile', isPrimary: true }],
emails: [],
// Property (Phase 4)
address: '',
@@ -130,14 +131,26 @@ export default function CreateLeadPage() {
const progressBarRef = useRef(null);
- // Derived: which fields must be filled for this mode
- const requiredFields = isQuickCapture
- ? ['leadSource']
- : ['leadSource', 'leadType', 'workType'];
+ // Section completion map
+ const sectionCompletion = {
+ contact: !!(
+ formData.firstName &&
+ formData.lastName &&
+ formData.phones.some(p => p.number.replace(/\D/g, '').length >= 10)
+ ),
+ property: false, // Phase 4
+ job: isQuickCapture
+ ? !!formData.leadSource
+ : !!(formData.leadSource && formData.leadType && formData.workType),
+ insurance: false, // Phase 5
+ assignment: false, // Phase 6
+ };
- const filledCount = requiredFields.filter(f => formData[f]).length;
- const progress = requiredFields.length > 0
- ? Math.round((filledCount / requiredFields.length) * 100)
+ // Progress = % of visible sections that are complete
+ const visibleSectionIds = (isQuickCapture ? SECTIONS.filter(s => s.quickCapture) : SECTIONS).map(s => s.id);
+ const completedCount = visibleSectionIds.filter(id => sectionCompletion[id]).length;
+ const progress = visibleSectionIds.length > 0
+ ? Math.round((completedCount / visibleSectionIds.length) * 100)
: 0;
// GSAP animates the progress bar whenever progress changes
@@ -151,17 +164,6 @@ export default function CreateLeadPage() {
}
}, [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 }));
};
@@ -170,9 +172,7 @@ export default function CreateLeadPage() {
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
};
- const visibleSections = isQuickCapture
- ? SECTIONS.filter(s => s.quickCapture)
- : SECTIONS;
+ const visibleSections = isQuickCapture ? SECTIONS.filter(s => s.quickCapture) : SECTIONS;
const modeSwitchTo = (quick) => {
if (quick === isQuickCapture) return;
@@ -184,6 +184,15 @@ export default function CreateLeadPage() {
// Render the content for a given section (placeholder until its phase is built)
const renderSectionContent = (section) => {
+ if (section.id === 'contact') {
+ return (
+
+ );
+ }
if (section.id === 'job') {
return (