let googleAccessToken = null; function signInWithGoogle() { return new Promise((resolve, reject) => { const client = google.accounts.oauth2.initTokenClient({ client_id: GOOGLE_CLIENT_ID, scope: GMAIL_SCOPE, callback: (response) => { if (response.error) { reject(response.error); return; } googleAccessToken = response.access_token; resolve(response.access_token); }, }); client.requestAccessToken(); }); } async function getGmailToken() { if (googleAccessToken) return googleAccessToken; return await signInWithGoogle(); } async function sbFetch(path, opts={}) { const res = await fetch(`${SUPABASE_URL}/rest/v1/${path}`, { ...opts, headers: { "apikey": SUPABASE_KEY, "Authorization": `Bearer ${SUPABASE_KEY}`, "Content-Type": "application/json", "Prefer": "return=representation", ...(opts.headers||{}) } }); if (!res.ok) throw new Error(await res.text()); const text = await res.text(); return text ? JSON.parse(text) : []; } function mapRow(c) { return { id: c.id, name: c.name || "", email: c.email || "", role: c.role || "", school: c.organization || "", district: c.district || "", state: c.region || "", persona: c.persona || "General", stage: c.pipeline_stage || "Cold", lastContact: c.last_touch || null, notes: c.notes || "", lead_source: c.lead_source || "", project_type: c.project_type || "", alt_contact: c.alt_contact || "", hunter_status: c.hunter_status || "", next_steps: c.next_steps || "", org_type: c.org_type || "K-12 School", is_strategic: c.is_strategic || false, relationship_goal: c.relationship_goal || "", stageEnteredAt: c.created_at || new Date().toISOString(), tasks: [], isUnsubscribed: false, bounced: c.pipeline_stage === "Bounced" || c.hunter_status === "bounced", }; } const C={lavender:"#DFC4DF",lightblue:"#C2E8FB",blue:"#40ACE2",green:"#65C39D",yellow:"#FAEE36",orange:"#F9A139",coral:"#EB5665",gDark:"#2d8f68",gBg:"#edf7f2",gBorder:"#b6dfc9",bBg:"#e8f4fc",bDark:"#1e7db0",bg:"#F5F6F8",surface:"#fff",border:"#E4E7EC",text:"#111827",textS:"#6B7280",textT:"#9CA3AF"}; // ── Click Tracker ───────────────────────────────────────────────────────────── const TRACKER_URL = "https://ig-click-tracker.maxwellrymsza.workers.dev"; const DOC_KEYS = { "1iTS_fMfxFGiojdCQKuS4Jr5FWlBwF7NK": "onepager", "1Ieb4I7Cc_N90pp5DRqNOLhxH8VxA7GZn": "specs", "1xRjpozVKQEy9TN3oRRy-o_nPNhYghCXC": "aeroponics", "1BVZQvNf6faTnr-qXn8bAV5S70LSBE4oH": "comparison", "1VLxxJbhJ__s82LXOCINT0iE2PLwAa_G3": "grant", "1ORU8mIgirKNORTRCcxO2n8a6LN4j6GM5": "deck", "1Qt6sSw9PYGbXZmiaM5iJ0QgORGU2KRXN": "curriculum", "1GFhPmQNAlmuw5b3xrMcbaIh_6BP6lXdl": "manual", "1Cqj-N3gkfe-uP_BI6nXroPtJo8PhcYvn": "experiments", "1CXYxzjLaoiEg2FOGSgq9HS1AirlHJnu-": "education", "1h_JcG9Ag-j34D1F37m6JVsjsuPR4sIRz": "services", "1RLhcXq_jL12TstL0bOZZLnD8zjxP-EhA": "cleaning", }; function trackedLink(contactId, docId) { const docKey = DOC_KEYS[docId]; if (!docKey) return null; return `${TRACKER_URL}/t/${contactId}-${docKey}`; } // ── CAN-SPAM Footer ─────────────────────────────────────────────────────────── function canSpamFooter(contact) { const unsubUrl = contact ? `https://ig-crm-take-2.vercel.app/unsubscribe?id=${contact.id}` : `https://ig-crm-take-2.vercel.app/unsubscribe`; return `-- Inspired Growing | 909 Newark Tpke, Kearny, NJ 07032 inspiredgrowing.com | info@inspiredgrowing.com You're receiving this because of your role in K-12 education. Unsubscribe: ${unsubUrl}`; } function canSpamFooterHtml(contact) { const unsubUrl = contact ? `https://ig-crm-take-2.vercel.app/unsubscribe?id=${contact.id}` : `https://ig-crm-take-2.vercel.app/unsubscribe`; return `

--
Inspired Growing | 909 Newark Tpke, Kearny, NJ 07032
inspiredgrowing.com

You're receiving this because of your role in K-12 education.
Unsubscribe
`; } // ── Knowledge Base Documents ────────────────────────────────────────────────── const KB_DOCS = [ {id:"1iTS_fMfxFGiojdCQKuS4Jr5FWlBwF7NK",title:"General One-Pager",file:"Overview_Key_Features.pdf",category:"Sales",tag:"One-Pager",tagColor:C.blue,tagBg:"#E6F1FB",description:"Key outcomes, aeroponics advantage, grant-readiness. Best all-purpose attach for cold outreach.",personas:["General","STEM","Nutrition","CTE","Sustainability"],viewUrl:"https://drive.google.com/file/d/1iTS_fMfxFGiojdCQKuS4Jr5FWlBwF7NK/view"}, {id:"1Ieb4I7Cc_N90pp5DRqNOLhxH8VxA7GZn",title:"Specs & Yields",file:"Specs_Safety_Requirements_Yields.pdf",category:"Sales",tag:"One-Pager",tagColor:C.blue,tagBg:"#E6F1FB",description:"Exact yield data — 240–336 servings/harvest, 7-14 day cycles, 75 sq ft footprint. Send to technical buyers.",personas:["STEM","General"],viewUrl:"https://drive.google.com/file/d/1Ieb4I7Cc_N90pp5DRqNOLhxH8VxA7GZn/view"}, {id:"1xRjpozVKQEy9TN3oRRy-o_nPNhYghCXC",title:"Aeroponics Technology Brief",file:"Aeroponics_Facts_Process.pdf",category:"Sales",tag:"One-Pager",tagColor:C.blue,tagBg:"#E6F1FB",description:"95% less water, NASA tech origin, how the misting system works. Great for science-forward contacts.",personas:["STEM","Sustainability"],viewUrl:"https://drive.google.com/file/d/1xRjpozVKQEy9TN3oRRy-o_nPNhYghCXC/view"}, {id:"1BVZQvNf6faTnr-qXn8bAV5S70LSBE4oH",title:"Competitive Comparison",file:"Comparison_Competition_Stats.pdf",category:"Sales",tag:"One-Pager",tagColor:C.blue,tagBg:"#E6F1FB",description:"IG vs tower & cabinet systems — 75 sq ft, 100 salads/week, 10+ year durability. Use when competing.",personas:["General","STEM"],viewUrl:"https://drive.google.com/file/d/1BVZQvNf6faTnr-qXn8bAV5S70LSBE4oH/view"}, {id:"1VLxxJbhJ__s82LXOCINT0iE2PLwAa_G3",title:"Grant Alignment Guide",file:"Grant_Info_For_Schools.pdf",category:"Sales",tag:"One-Pager",tagColor:"#0F6E56",tagBg:"#E1F5EE",description:"How IG maps to grantmaker priorities — STEM, farm-to-school, food deserts, workforce dev. Essential for Nutrition contacts.",personas:["Nutrition","Sustainability","CTE"],viewUrl:"https://drive.google.com/file/d/1VLxxJbhJ__s82LXOCINT0iE2PLwAa_G3/view"}, {id:"1ORU8mIgirKNORTRCcxO2n8a6LN4j6GM5",title:"Full Overview Slideshow",file:"General_Overview_Slideshow.pdf",category:"Sales",tag:"Deck",tagColor:"#854F0B",tagBg:"#FAEEDA",description:"Complete pitch deck — founder story, AeroFarms DNA, quotes from Newark BOE & Riverdale. Best for warm leads.",personas:["General","STEM","Nutrition","CTE","Sustainability"],viewUrl:"https://drive.google.com/file/d/1ORU8mIgirKNORTRCcxO2n8a6LN4j6GM5/view"}, {id:"1Qt6sSw9PYGbXZmiaM5iJ0QgORGU2KRXN",title:"Curriculum Overview",file:"Curriculum_Features_Extensions.pdf",category:"Education",tag:"One-Pager",tagColor:"#3B6D11",tagBg:"#EAF3DE",description:"NGSS-aligned, 6 lessons, student journals, special needs angle. Send to STEM coordinators and science teachers.",personas:["STEM","CTE"],viewUrl:"https://drive.google.com/file/d/1Qt6sSw9PYGbXZmiaM5iJ0QgORGU2KRXN/view"}, {id:"1GFhPmQNAlmuw5b3xrMcbaIh_6BP6lXdl",title:"User Manual",file:"User_Manual_Specs_Facts.pdf",category:"Technical",tag:"Manual",tagColor:"#5B21B6",tagBg:"#EDE9FE",description:"Complete operational manual — equipment, safety, growing process, glossary. Share post-sale or with technical reviewers.",personas:["General"],viewUrl:"https://drive.google.com/file/d/1GFhPmQNAlmuw5b3xrMcbaIh_6BP6lXdl/view"}, {id:"1Cqj-N3gkfe-uP_BI6nXroPtJo8PhcYvn",title:"Living Light Lab Experiments",file:"Premium_Experiments_Ideas.pdf",category:"Education",tag:"Premium",tagColor:"#854F0B",tagBg:"#FAEEDA",description:"Light spectrum experiments across grade levels — lower, middle, high school. Great for engaged STEM schools.",personas:["STEM"],viewUrl:"https://drive.google.com/file/d/1Cqj-N3gkfe-uP_BI6nXroPtJo8PhcYvn/view"}, {id:"1CXYxzjLaoiEg2FOGSgq9HS1AirlHJnu-",title:"Educational Service Program",file:"Premium_Education_Service.pdf",category:"Services",tag:"Service",tagColor:"#185FA5",tagBg:"#E6F1FB",description:"IG teaching programs — K-5, 6-8, 9-12 formats. In-class, after-school, one-time workshops.",personas:["STEM","CTE","Nutrition"],viewUrl:"https://drive.google.com/file/d/1CXYxzjLaoiEg2FOGSgq9HS1AirlHJnu-/view"}, {id:"1h_JcG9Ag-j34D1F37m6JVsjsuPR4sIRz",title:"Expanded Services Overview",file:"Premium_Services_Overview.pdf",category:"Services",tag:"Service",tagColor:"#185FA5",tagBg:"#E6F1FB",description:"Farming support, kids programs, workshops & events, enhanced cleaning. For current and prospective customers.",personas:["General","Nutrition"],viewUrl:"https://drive.google.com/file/d/1h_JcG9Ag-j34D1F37m6JVsjsuPR4sIRz/view"}, {id:"1RLhcXq_jL12TstL0bOZZLnD8zjxP-EhA",title:"Farm Cleaning & Maintenance",file:"Premium_Cleaning_Service.pdf",category:"Services",tag:"Service",tagColor:"#185FA5",tagBg:"#E6F1FB",description:"Maintenance services for non-IG systems — deep cleaning, preventive maintenance, troubleshooting.",personas:["General"],viewUrl:"https://drive.google.com/file/d/1RLhcXq_jL12TstL0bOZZLnD8zjxP-EhA/view"}, ]; const KB_CATEGORIES = ["All", "Sales", "Education", "Technical", "Services"]; const TITLES = new Set(['dr','dr.','mr','mr.','mrs','mrs.','ms','ms.','prof','prof.','rev','rev.','sr','sr.','jr','jr.','phd','md','esq','hon']); function getFirstName(name){ if(!name) return "there"; const parts = name.trim().split(/\s+/); // Skip leading titles let i = 0; while(i < parts.length - 1 && TITLES.has(parts[i].toLowerCase())) i++; return parts[i] || "there"; } const DEF_SIG="Sincerely,\n\nEric Wagner\nInspired Growing\ninspiredgrowing.com"; const DEF_CLOSE="We've also just updated our website to better tell the story of how our farms are supporting communities like yours. Take a look at inspiredgrowing.com! If you'd like to learn more, I'm happy to send over some details or hop on a quick 15-minute call whenever it's convenient for you."; const DEF_CLOSE_FOLLOWUP="Would you be open to a quick call to hear more about what we do? Happy to keep it brief."; const SL=()=>({fontSize:10,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.08em",color:C.textT,padding:"16px 16px 6px",userSelect:"none"}); const NOW=Date.now(); const STAGES=["Cold","Emailed","Direct Contact","Replied","Warm","Demo Scheduled","Proposal Sent","Closed Won","Existing Client","Closed Lost","Bounced","Declined"]; const STAGE_META={"Cold":{color:"#9CA3AF",bg:"#F3F4F6",icon:"❄️"},"Emailed":{color:"#185FA5",bg:"#E6F1FB",icon:"📧"},"Direct Contact":{color:"#7C3AED",bg:"#EDE9FE",icon:"🤝"},"Replied":{color:"#0F6E56",bg:"#E1F5EE",icon:"💬"},"Warm":{color:"#EA580C",bg:"#FFF7ED",icon:"🔥"},"Demo Scheduled":{color:"#854F0B",bg:"#FAEEDA",icon:"📅"},"Proposal Sent":{color:"#5B21B6",bg:"#EDE9FE",icon:"📄"},"Closed Won":{color:"#065F46",bg:"#D1FAE5",icon:"🏆"},"Existing Client":{color:"#0E7490",bg:"#ECFEFF",icon:"🌱"},"Closed Lost":{color:"#A32D2D",bg:"#FEE2E2",icon:"✗"},"Bounced":{color:"#A32D2D",bg:"#FEE2E2",icon:"↩️"},"Declined":{color:"#6B7280",bg:"#F3F4F6",icon:"✗"},"Contacted":{color:"#185FA5",bg:"#E6F1FB",icon:"📧"}}; const STUCK_DAYS={Cold:21,Emailed:7,"Direct Contact":14,Replied:5,Warm:14,"Demo Scheduled":7,"Proposal Sent":14}; const PER_C={STEM:["#1D4ED8","#EFF6FF"],Nutrition:["#065F46","#ECFDF5"],Sustainability:["#166534","#F0FDF4"],"CTE":["#92400E","#FFFBEB"],"CTE / Culinary":["#92400E","#FFFBEB"],General:["#6B7280","#F1EFE8"],Administrator:["#6B21A8","#F3E8FF"]}; const TMPL={ STEM:{label:"STEM",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. Students run their own experimental grow cycles, generate real data, and iterate on results — all within a single term. The curriculum is NGSS-aligned and connects naturally to biology, chemistry, and environmental science."}, Nutrition:{label:"Nutrition",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. Students grow, harvest, and eat what they produce — the farm-to-school distance is exactly zero. No soil, no pesticides, no food miles."}, Sustainability:{label:"Sustainability",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. The systems run without soil, pesticides, or food miles — and give students a hands-on understanding of where food comes from and how sustainable agriculture works at scale."}, CTE:{label:"CTE / Culinary",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. Students run the full cycle from seed to harvest to kitchen — developing real skills in agriculture, food science, and culinary arts within a single program."}, General:{label:"General",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. The system connects naturally to STEM curriculum, nutrition goals, and sustainability programming — and fits in 50 square feet on standard outlets with no plumbing required."}, Administrator:{label:"Administrator",roleBlurb:"Inspired Growing brings commercial-grade aeroponic farms into schools and community spaces. We currently have farms operating in more than 30 schools across New Jersey and New York. Using the same technology developed for AeroFarms' large-scale vertical farms, our systems are scaled to fit in as little as 50 square feet while producing up to 1,000 pounds of fresh leafy greens annually. Depending on the crop, plants can be harvested in as little as seven days. The program connects to STEM curriculum, supports nutrition goals, and has become one of the more talked-about initiatives in the districts we work with. It runs on standard outlets in 50 square feet with no plumbing."}}; const fmtTime=iso=>{const d=Math.floor((NOW-new Date(iso).getTime())/86400000);return d===0?"Today":d===1?"Yesterday":`${d}d ago`;}; const fmtDate=iso=>new Date(iso).toLocaleDateString("en-US",{month:"short",day:"numeric"}); const fmtDT=iso=>new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}); function Tag({label,color,bg,size=11}){return {label};} function Av({name,size=32,color=C.blue}){const initials=(name||"?").split(" ").map(x=>x[0]).join("").toUpperCase().slice(0,2);return
{initials}
;} const B=(v="primary",sz="md")=>{const base={border:"none",borderRadius:8,cursor:"pointer",fontWeight:600,display:"inline-flex",alignItems:"center",justifyContent:"center",gap:6,transition:"opacity .15s"};const sizes={sm:{fontSize:12,padding:"5px 12px"},md:{fontSize:13,padding:"8px 16px"},lg:{fontSize:14,padding:"10px 20px"}};const variants={primary:{background:C.green,color:"white"},blue:{background:C.blue,color:"white"},secondary:{background:"white",color:C.text,border:`1px solid ${C.border}`},ghost:{background:"transparent",color:C.textS},danger:{background:"#FEE2E2",color:"#A32D2D",border:"1px solid #FECACA"}};return{...base,...sizes[sz]||sizes.md,...variants[v]||variants.primary};}; const I=(w)=>({width:w||"100%",padding:"8px 12px",border:`1px solid ${C.border}`,borderRadius:8,fontSize:13,outline:"none",color:C.text,background:"white",boxSizing:"border-box"}); const S=()=>({padding:"7px 10px",border:`1px solid ${C.border}`,borderRadius:8,fontSize:13,outline:"none",color:C.text,background:"white",cursor:"pointer"}); const LBL=()=>({fontSize:10,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.05em",color:C.textT}); const TH=(w)=>({padding:"10px 12px",textAlign:"left",fontSize:11,fontWeight:600,color:C.textS,textTransform:"uppercase",letterSpacing:"0.05em",whiteSpace:"nowrap",...(w?{width:w}:{})}); const TD=(w)=>({padding:"10px 12px",fontSize:13,verticalAlign:"middle",...(w?{width:w}:{})}); const CAMPAIGNS={ cold:{id:"cold",label:"Cold Outreach",icon:"❄️",description:"AI personalized intro · district hook · persona pitch",subjectFn:(c)=>{ const p=c.persona; const school=c.school||c.district||"your school"; if(p==="STEM") return `A hands-on STEM lab for ${school}`; if(p==="Nutrition") return `A farm-to-table nutrition program for ${school}`; if(p==="Sustainability") return `A sustainable food production program for ${school}`; if(p==="CTE"||p==="CTE / Culinary") return `A live agriculture lab for ${school}`; if(p==="Administrator") return `A hands-on STEM and nutritional literacy system for ${school}`; return `A program worth knowing about for ${school}`; },ai:true}, customer:{id:"customer",label:"Customer Update",icon:"🌱",description:"Summer reconnection · new features · premium services",subjectFn:(c)=>`Summer Updates from Inspired Growing`,ai:false}, followup:{id:"followup",label:"Follow-Up",icon:"📬",description:"Check in after no response · low pressure nudge",subjectFn:(c)=>`Re: A program worth knowing about for ${c.school||c.district}`,ai:true}, custom:{id:"custom",label:"Custom",icon:"✏️",description:"Paste your own template · use [PLACEHOLDERS] · fills from contact record",subjectFn:(c,subj)=>subj||"",ai:false}, }; // ── New Leads Page ──────────────────────────────────────────────────────────── function NewLeadsPage({contacts,onCompose}){ const sevenDaysAgo=Date.now()-7*24*60*60*1000; const inbound=contacts.filter(c=>new Date(c.stageEnteredAt||0).getTime()>sevenDaysAgo).sort((a,b)=>new Date(b.stageEnteredAt||0)-new Date(a.stageEnteredAt||0)); const fmtDT2=iso=>{try{return new Date(iso).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"});}catch(e){return iso||"Unknown";}}; return <>
New Leads
All new contacts added in the last 7 days · {inbound.length} total
{inbound.length===0&&
📬
No new contacts in the last 7 days
New contacts added manually or from your website will appear here
} {inbound.map(c=>{ const[pc,pb]=PER_C[c.persona]||["#6B7280","#F1EFE8"]; const isNew=c.stageEnteredAt&&(Date.now()-new Date(c.stageEnteredAt).getTime())<7*24*60*60*1000; return
{isNew?"🔔":"✓"}
{c.name||c.email||"Unknown"}
{isNew&&NEW}
{c.email}
{c.persona} {c.lead_source} {fmtDT2(c.stageEnteredAt)}
{c.notes&&
"{c.notes}"
}
; })}
; } function Spinner({size=12}){return
;} function EnrollModal({seq,contacts,enrollments,onClose,onEnroll}){ const[search,setSearch]=useState(""); const[sel,setSel]=useState(new Set()); const alreadyEnrolled=new Set(enrollments.filter(e=>e.sequence_id===seq.id).map(e=>e.contact_id)); const eligible=contacts.filter(c=>!c.isUnsubscribed&&!c.bounced&&!alreadyEnrolled.has(c.id)&&(!search||`${c.name} ${c.district} ${c.role}`.toLowerCase().includes(search.toLowerCase()))); function toggle(id){setSel(p=>{const s=new Set(p);s.has(id)?s.delete(id):s.add(id);return s;});} return
e.stopPropagation()} style={{background:"white",borderRadius:12,width:520,maxHeight:"80vh",display:"flex",flexDirection:"column",boxShadow:"0 20px 60px rgba(0,0,0,0.2)",overflow:"hidden"}}>
Enroll Contacts
into "{seq.name}"
setSearch(e.target.value)} placeholder="🔍 Search contacts..." style={{width:"100%",padding:"7px 10px",border:`1px solid #E4E7EC`,borderRadius:6,fontSize:13,outline:"none"}}/>
{eligible.map(c=>{const[pc,pb]=PER_C[c.persona]||["#6B7280","#F1EFE8"];const chk=sel.has(c.id);return
toggle(c.id)} style={{display:"flex",alignItems:"center",gap:10,padding:"8px 16px",cursor:"pointer",background:chk?"#edf7f2":"white",borderBottom:`1px solid #E4E7EC`}} onMouseEnter={e=>e.currentTarget.style.background=chk?"#e0f3ec":"#f9fffe"} onMouseLeave={e=>e.currentTarget.style.background=chk?"#edf7f2":"white"}>{}} style={{accentColor:"#65C39D",cursor:"pointer",flexShrink:0}}/>
{(c.name||"?").split(" ").map(x=>x[0]).join("").toUpperCase().slice(0,2)}
{c.name}
{c.role} · {c.district}
{c.persona}
;})} {eligible.length===0&&
No eligible contacts found
}
{sel.size} selected · {eligible.length} eligible
; } function SequencesPage({contacts,sequences,setSequences,enrollments,setEnrollments,seqLog,setSeqLog}){ const[newOpen,setNewOpen]=useState(false); const[newName,setNewName]=useState("Cold Outreach — Standard"); const[newInterval,setNewInterval]=useState(7); const[newBrake,setNewBrake]=useState(50); const[saving,setSaving]=useState(false); const[enrollTarget,setEnrollTarget]=useState(null); async function createSeq(){ const tmpl=SEQUENCE_TEMPLATES[newName]; if(!tmpl)return; setSaving(true); try{ const rows=await sbFetch("sequences",{method:"POST",body:JSON.stringify({ name:newName, template_id:tmpl.id, status:"paused", interval_minutes:newInterval, emergency_brake_threshold:newBrake, started_at:new Date().toISOString() })}); setSequences(p=>[...p,rows[0]]); setNewOpen(false); }catch(e){alert("Error creating sequence: "+e.message);} setSaving(false); } async function toggleStatus(seq){ const newStatus=seq.status==="active"?"paused":"active"; await sbFetch(`sequences?id=eq.${seq.id}`,{method:"PATCH",body:JSON.stringify({status:newStatus})}); setSequences(p=>p.map(s=>s.id===seq.id?{...s,status:newStatus}:s)); } async function brakeSeq(seq){ await sbFetch(`sequences?id=eq.${seq.id}`,{method:"PATCH",body:JSON.stringify({status:"braked"})}); setSequences(p=>p.map(s=>s.id===seq.id?{...s,status:"braked"}:s)); } async function resetBrake(seq){ await sbFetch(`sequences?id=eq.${seq.id}`,{method:"PATCH",body:JSON.stringify({status:"paused"})}); setSequences(p=>p.map(s=>s.id===seq.id?{...s,status:"paused"}:s)); } async function handleEnroll(seqId, contactIds){ const now=new Date().toISOString(); const rows=contactIds.map(cid=>({ sequence_id:seqId, contact_id:cid, current_step:0, status:"active", enrolled_at:now, next_send_at:now })); try{ const inserted=await sbFetch("enrollments",{method:"POST",body:JSON.stringify(rows)}); setEnrollments(p=>[...p,...inserted]); // Update pipeline stage to Contacted for Cold contacts for(const cid of contactIds){ const c=contacts.find(x=>x.id===cid); if(c&&c.stage==="Cold"){ await sbFetch(`contacts?id=eq.${cid}`,{method:"PATCH",body:JSON.stringify({pipeline_stage:"Contacted"})}); } } }catch(e){alert("Error enrolling: "+e.message);} } return <>
Sequences
Automated drip sequences · staggered sending · review queue
Slow Drip Mode — Active
Emails send 1 at a time with a configurable gap (default: every 7 minutes). This mimics human sending behavior and avoids Gmail's automated spam filters. The Emergency Brake automatically pauses any sequence if bounce rate exceeds threshold or an API error is detected.
{sequences.length===0&&
🔄
No sequences yet
Create your first sequence to start automating outreach
} {sequences.map(seq=>{ const tmpl=Object.values(SEQUENCE_TEMPLATES).find(t=>t.id===seq.template_id)||Object.values(SEQUENCE_TEMPLATES)[0]; const seqEnrollments=enrollments.filter(e=>e.sequence_id===seq.id); const activeEnrollments=seqEnrollments.filter(e=>e.status==="active"); const sentCount=seqLog.filter(l=>l.sequence_id===seq.id).length; const recentLogs=seqLog.filter(l=>l.sequence_id===seq.id).slice(0,4); const sm={active:{color:C.gDark,bg:C.gBg,label:"● Active"},paused:{color:"#854F0B",bg:"#FAEEDA",label:"⏸ Paused"},braked:{color:"#A32D2D",bg:"#FEE2E2",label:"🛑 Emergency Brake"}}[seq.status]||{color:"#854F0B",bg:"#FAEEDA",label:"⏸ Paused"}; return
{seq.status==="braked"&&
🛑Emergency Brake Triggered — Review logs before resuming.
}
{seq.name}
{sm.label}
{tmpl?.description}
{seq.status!=="braked"&&}
{[["Enrolled",seqEnrollments.length,"#185FA5","#E6F1FB"],["Active",activeEnrollments.length,C.gDark,C.gBg],["Sent",sentCount,"#5B21B6","#EDE9FE"],["Completed",seqEnrollments.filter(e=>e.status==="completed").length,"#9CA3AF","#F3F4F6"]].map(([l,v,col,bg])=>(
{v}
{l}
))}
{seq.interval_minutes} min between sends · 🛑 Brake at {seq.emergency_brake_threshold} sends · Started {fmtTime(seq.started_at||seq.created_at)}
Sequence steps
0?12:0}}> {tmpl?.steps.map((step,i)=>(
Day {step.day}: {step.label}
{step.approvalRequired&&APPROVAL} {step.type.replace(/_/g," ")}
{i→}
))}
{recentLogs.length>0&&<>
Recent sends
{recentLogs.map((l,i)=>(
{l.contact_name} {l.step_label} {fmtTime(l.sent_at)}
))} }
; })}
{newOpen&&
setNewOpen(false)} style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.4)",zIndex:300,display:"flex",alignItems:"center",justifyContent:"center"}}>
e.stopPropagation()} style={{background:"white",borderRadius:12,width:500,boxShadow:"0 20px 60px rgba(0,0,0,0.2)",padding:24}}>
New Sequence
{SEQUENCE_TEMPLATES[newName]?.description}
setNewInterval(Number(e.target.value))} min={1} max={60} style={I()}/>
Recommended: 5–10 min
setNewBrake(Number(e.target.value))} min={1} max={200} style={I()}/>
Max sends before auto-pause
⚠ All emails include a CAN-SPAM compliant footer with your address and a personalized unsubscribe link.
} {enrollTarget&&setEnrollTarget(null)} onEnroll={handleEnroll} />} ; } // ── Review Queue Page ───────────────────────────────────────────────────────── function ReviewQueuePage({contacts,enrollments,seqLog,setSeqLog,sequences}){ const[selected,setSelected]=useState(null); const[approving,setApproving]=useState(false); // Build a queue from active enrollments that need approval for their current step const queue = enrollments .filter(e=>e.status==="active") .map(e=>{ const c=contacts.find(x=>x.id===e.contact_id); const seq=sequences.find(x=>x.id===e.sequence_id); if(!c||!seq)return null; const tmpl=Object.values(SEQUENCE_TEMPLATES).find(t=>t.id===seq.template_id); const step=tmpl?.steps[e.current_step]; if(!step||!step.approvalRequired)return null; const alreadySent=seqLog.find(l=>l.sequence_id===seq.id&&l.contact_id===c.id&&l.step_label===step.label); if(alreadySent)return null; return { enrollmentId:e.id, sequenceId:seq.id, sequenceName:seq.name, contactId:c.id, contactName:c.name, contactDistrict:c.district, contactPersona:c.persona, stepLabel:step.label, stepIndex:e.current_step, subject:`A program worth knowing about for ${c.school||c.district}`, scheduledFor:e.next_send_at||new Date().toISOString(), status:"pending", body:`Hi ${getFirstName(c.name)},\n\n[AI-generated hook for ${c.district} will appear here]\n\nInspired Growing installs compact aeroponic classroom farms in K-12 schools — students grow and harvest fresh greens every 7 days, producing around 100 salads a week right in the building.\n\nWould it be worth a quick 10-minute call?\n\nEric Wagner\nInspired Growing\ninspiredgrowing.com\n\n${canSpamFooter(c.id)}`, }; }).filter(Boolean); const [editBodies, setEditBodies] = useState({}); async function approve(item){ setApproving(true); try{ // Log the send const logRow={ sequence_id:item.sequenceId, contact_id:item.contactId, contact_name:item.contactName, step_label:item.stepLabel, subject:item.subject, status:"sent", sent_at:new Date().toISOString() }; const inserted=await sbFetch("sequence_log",{method:"POST",body:JSON.stringify(logRow)}); setSeqLog(p=>[inserted[0],...p]); // Advance enrollment step await sbFetch(`enrollments?id=eq.${item.enrollmentId}`,{method:"PATCH",body:JSON.stringify({current_step:item.stepIndex+1})}); // Update contact pipeline stage await sbFetch(`contacts?id=eq.${item.contactId}`,{method:"PATCH",body:JSON.stringify({pipeline_stage:"Contacted",last_touch:new Date().toISOString().split("T")[0]})}); }catch(e){alert("Error approving: "+e.message);} setApproving(false); setSelected(null); } async function reject(item){ // Mark enrollment as paused for this contact await sbFetch(`enrollments?id=eq.${item.enrollmentId}`,{method:"PATCH",body:JSON.stringify({status:"paused"})}); setSelected(null); } return <>
Review Queue
Approve or reject emails before they send
{queue.length>0&&{queue.length} awaiting approval}
{queue.length===0&&
📬
No emails pending review
} {queue.map(q=>(
setSelected(q)} style={{padding:"10px 14px",borderBottom:`1px solid ${C.border}`,cursor:"pointer",background:selected?.enrollmentId===q.enrollmentId?C.gBg:"white",borderLeft:selected?.enrollmentId===q.enrollmentId?`3px solid ${C.green}`:"3px solid transparent"}} onMouseEnter={e=>{if(selected?.enrollmentId!==q.enrollmentId)e.currentTarget.style.background=C.bg;}} onMouseLeave={e=>{if(selected?.enrollmentId!==q.enrollmentId)e.currentTarget.style.background="white";}}>
{q.contactName}
{q.contactDistrict}
{q.stepLabel} · {q.sequenceName}
Scheduled: {fmtDT(q.scheduledFor)}
))}
{!selected?(
📬
Select an email to review
Review and approve each email before it sends. You can edit the body before approving.
):<>
{selected.contactName}
{selected.contactDistrict} · {selected.sequenceName} · {selected.stepLabel}
{selected.subject}
📅 Scheduled: {fmtDT(selected.scheduledFor)}