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 */} +
+ + + + +
+ + {/* 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 ( + + ); +} + +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 && ( + + + + )} + +
+ + {/* ---- 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. +

+ )} + + +
+
+ )} +
+
+ ); +} 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 && ( +
+ + + + +
+ )} + + {/* 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 (