
Administration and Technology
import { useState, useMemo } from "react"; // ── Seed Data ────────────────────────────────────────────────────────────── const INITIAL_CHURCHES = [ { id: 1, name: "First Baptist Midlothian", pastor: "Rev. James Whitfield", members: 412, city: "Midlothian", phone: "804-555-0101", email: "firstbaptistmid@example.com", lastContact: "2026-05-20", engagementScore: 92, status: "Active", tags: ["Anchor", "Missions"], notes: "Strong missions team. Hosts quarterly pastor lunch." }, { id: 2, name: "Grace Baptist Fellowship", pastor: "Rev. Maria Santos", members: 187, city: "Chesterfield", phone: "804-555-0202", email: "gracebaptistfell@example.com", lastContact: "2026-04-10", engagementScore: 64, status: "Active", tags: ["Youth Ministry"], notes: "Growing youth program. Needs encouragement on giving." }, { id: 3, name: "Calvary Baptist Church", pastor: "Rev. Thomas Hill", members: 230, city: "Bon Air", phone: "804-555-0303", email: "calvarybaptist@example.com", lastContact: "2026-03-01", engagementScore: 38, status: "At Risk", tags: [], notes: "Minimal association contact last 6 months. Schedule visit." }, { id: 4, name: "New Hope Baptist", pastor: "Rev. Linda Carter", members: 95, city: "Powhatan", phone: "804-555-0404", email: "newhopebaptist@example.com", lastContact: "2026-05-28", engagementScore: 85, status: "Active", tags: ["Worship", "Missions"], notes: "Eager to partner. Interested in church planting network." }, { id: 5, name: "Riverside Baptist", pastor: "Rev. Samuel Brooks", members: 310, city: "Midlothian", phone: "804-555-0505", email: "riversidebaptist@example.com", lastContact: "2026-02-14", engagementScore: 22, status: "Disengaged", tags: [], notes: "No response to last 3 outreach attempts. Needs personal visit." }, { id: 6, name: "Cornerstone Baptist", pastor: "Rev. Angela Moore", members: 155, city: "Colonial Heights", phone: "804-555-0606", email: "cornerstonebaptist@example.com", lastContact: "2026-05-05", engagementScore: 71, status: "Active", tags: ["Stewardship"], notes: "Consistent giver. Would benefit from leadership training." }, { id: 7, name: "Bethel Baptist Church", pastor: "Rev. Darius King", members: 278, city: "Petersburg", phone: "804-555-0707", email: "bethelbaptist@example.com", lastContact: "2026-04-22", engagementScore: 55, status: "Active", tags: ["Evangelism"], notes: "Active evangelism. Irregularly attends association events." }, { id: 8, name: "Pilgrim Rest Baptist", pastor: "Rev. Dorothy Evans", members: 68, city: "Amelia", phone: "804-555-0808", email: "pilgrimrest@example.com", lastContact: "2026-01-30", engagementScore: 15, status: "Disengaged", tags: [], notes: "Small congregation facing challenges. Reach out immediately." }, ]; const INITIAL_GIVING = [ { id: 1, churchId: 1, churchName: "First Baptist Midlothian", date: "2026-05-01", amount: 2800, category: "Cooperative Program", note: "Monthly CP gift" }, { id: 2, churchId: 2, churchName: "Grace Baptist Fellowship", date: "2026-05-01", amount: 950, category: "Association Budget", note: "" }, { id: 3, churchId: 4, churchName: "New Hope Baptist", date: "2026-05-01", amount: 500, category: "Missions Fund", note: "Spring missions offering" }, { id: 4, churchId: 6, churchName: "Cornerstone Baptist", date: "2026-05-01", amount: 1200, category: "Cooperative Program", note: "" }, { id: 5, churchId: 1, churchName: "First Baptist Midlothian", date: "2026-04-01", amount: 2800, category: "Cooperative Program", note: "" }, { id: 6, churchId: 7, churchName: "Bethel Baptist Church", date: "2026-04-01", amount: 700, category: "Association Budget", note: "" }, { id: 7, churchId: 3, churchName: "Calvary Baptist Church", date: "2026-04-01", amount: 400, category: "Cooperative Program", note: "Partial gift" }, { id: 8, churchId: 2, churchName: "Grace Baptist Fellowship", date: "2026-04-01", amount: 950, category: "Association Budget", note: "" }, { id: 9, churchId: 6, churchName: "Cornerstone Baptist", date: "2026-04-01", amount: 1200, category: "Cooperative Program", note: "" }, { id: 10, churchId: 1, churchName: "First Baptist Midlothian", date: "2026-03-01", amount: 2800, category: "Cooperative Program", note: "" }, { id: 11, churchId: 4, churchName: "New Hope Baptist", date: "2026-03-01", amount: 500, category: "Missions Fund", note: "" }, ]; const INITIAL_TOUCHPOINTS = [ { id: 1, churchId: 3, type: "Visit", date: "2026-06-15", description: "Schedule in-person visit with Rev. Hill", completed: false }, { id: 2, churchId: 5, type: "Phone Call", date: "2026-06-12", description: "Director to call Rev. Brooks personally", completed: false }, { id: 3, churchId: 8, type: "Visit", date: "2026-06-20", description: "Pastoral care visit - small congregation in need", completed: false }, { id: 4, churchId: 2, type: "Event Invite", date: "2026-06-18", description: "Invite to summer leadership retreat", completed: false }, ]; const CATEGORIES = ["Cooperative Program", "Association Budget", "Missions Fund", "Special Offering", "Disaster Relief"]; const TOUCHPOINT_TYPES = ["Phone Call", "Visit", "Email", "Event Invite", "Letter", "Prayer"]; // ── Helpers ──────────────────────────────────────────────────────────────── const scoreColor = (s) => s >= 75 ? "#6B8F71" : s >= 40 ? "#C9A84C" : "#C0392B"; const scoreLabel = (s) => s >= 75 ? "Healthy" : s >= 40 ? "Needs Attention" : "Disengaged"; const daysSince = (dateStr) => { const diff = (new Date() - new Date(dateStr)) / (1000 * 60 * 60 * 24); return Math.floor(diff); }; const fmt = (n) => "$" + n.toLocaleString(); const fmtDate = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); // ── Ring SVG ──────────────────────────────────────────────────────────────── function EngagementRing({ score, size = 56 }) { const r = (size - 8) / 2, circ = 2 * Math.PI * r; const dash = (score / 100) * circ; return ( ); } // ── Giving Bar Chart ──────────────────────────────────────────────────────── function GivingBar({ data }) { const max = Math.max(...data.map(d => d.total), 1); return (
); // ── Churches ───────────────────────────────────────────────────────── const ChurchesTab = () => (
{filteredChurches.map(c => (
); // ── Strategy ───────────────────────────────────────────────────────── const StrategyTab = () => { const disengagedChurches = churches.filter(c => c.engagementScore < 40); const needsAttn = churches.filter(c => c.engagementScore >= 40 && c.engagementScore < 75); const healthy = churches.filter(c => c.engagementScore >= 75); return (
{/* Priority tiers */}
))}
))}
))}
{/* Touchpoint Log */}
{!tp.completed && } {tp.completed && ✓ Completed}
); })}
); }; // ── Giving ───────────────────────────────────────────────────────────── const GivingTab = () => (
{fmt(total)}
))}
); // ── Church Detail Modal ───────────────────────────────────────────────── const ChurchModal = () => { const c = selectedChurch; const churchGiving = giving.filter(g=>g.churchId===c.id); const churchTotal = churchGiving.reduce((s,g)=>s+g.amount,0); return (
)} {/* Add Touchpoint Modal */} {showAddTouch && (
)} {/* Add Church Modal */} {showAddChurch && (
)}
); }
{data.map((d, i) => (
); } // ── Main App ─────────────────────────────────────────────────────────────── export default function App() { const [tab, setTab] = useState("dashboard"); const [churches, setChurches] = useState(INITIAL_CHURCHES); const [giving, setGiving] = useState(INITIAL_GIVING); const [touchpoints, setTouchpoints] = useState(INITIAL_TOUCHPOINTS); const [selectedChurch, setSelectedChurch] = useState(null); const [showAddChurch, setShowAddChurch] = useState(false); const [showAddGiving, setShowAddGiving] = useState(false); const [showAddTouch, setShowAddTouch] = useState(false); const [givingFilter, setGivingFilter] = useState("All"); const [churchSearch, setChurchSearch] = useState(""); const [newGiving, setNewGiving] = useState({ churchId: "", date: new Date().toISOString().slice(0,10), amount: "", category: "Cooperative Program", note: "" }); const [newTouch, setNewTouch] = useState({ churchId: "", type: "Phone Call", date: "", description: "" }); const [newChurch, setNewChurch] = useState({ name: "", pastor: "", members: "", city: "", phone: "", email: "" }); // Computed stats const totalGiving = useMemo(() => giving.reduce((s, g) => s + g.amount, 0), [giving]); const thisMonthGiving = useMemo(() => giving.filter(g => g.date.startsWith("2026-05")).reduce((s, g) => s + g.amount, 0), [giving]); const disengaged = churches.filter(c => c.engagementScore < 40).length; const pendingTouchpoints = touchpoints.filter(t => !t.completed).length; const monthlyGiving = useMemo(() => { const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]; const prefixes = ["2026-01","2026-02","2026-03","2026-04","2026-05","2026-06"]; return prefixes.map((p, i) => ({ month: months[i], total: giving.filter(g => g.date.startsWith(p)).reduce((s,g)=>s+g.amount,0) })); }, [giving]); const givingByChurch = useMemo(() => { const map = {}; giving.forEach(g => { map[g.churchName] = (map[g.churchName] || 0) + g.amount; }); return Object.entries(map).sort((a,b)=>b[1]-a[1]); }, [giving]); const filteredGiving = useMemo(() => givingFilter === "All" ? giving : giving.filter(g => g.category === givingFilter), [giving, givingFilter]); const filteredChurches = useMemo(() => churches.filter(c => c.name.toLowerCase().includes(churchSearch.toLowerCase()) || c.city.toLowerCase().includes(churchSearch.toLowerCase())), [churches, churchSearch]); const addGiving = () => { if (!newGiving.churchId || !newGiving.amount) return; const church = churches.find(c => c.id === parseInt(newGiving.churchId)); setGiving(g => [...g, { id: Date.now(), churchId: parseInt(newGiving.churchId), churchName: church?.name || "", ...newGiving, amount: parseFloat(newGiving.amount) }]); setNewGiving({ churchId: "", date: new Date().toISOString().slice(0,10), amount: "", category: "Cooperative Program", note: "" }); setShowAddGiving(false); }; const addTouchpoint = () => { if (!newTouch.churchId || !newTouch.date) return; setTouchpoints(t => [...t, { id: Date.now(), ...newTouch, churchId: parseInt(newTouch.churchId), completed: false }]); setNewTouch({ churchId: "", type: "Phone Call", date: "", description: "" }); setShowAddTouch(false); }; const addChurch = () => { if (!newChurch.name || !newChurch.pastor) return; setChurches(c => [...c, { id: Date.now(), ...newChurch, members: parseInt(newChurch.members)||0, engagementScore: 50, status: "Active", tags: [], notes: "", lastContact: new Date().toISOString().slice(0,10) }]); setNewChurch({ name: "", pastor: "", members: "", city: "", phone: "", email: "" }); setShowAddChurch(false); }; const completeTouchpoint = (id) => setTouchpoints(t => t.map(tp => tp.id === id ? {...tp, completed: true} : tp)); // ── Styles ───────────────────────────────────────────────────────────── const styles = { app: { fontFamily: "Inter, sans-serif", background: "#F7F4EE", minHeight: "100vh", color: "#1B2A4A" }, header: { background: "#1B2A4A", padding: "0 24px", display: "flex", alignItems: "center", justifyContent: "space-between", height: 60, boxShadow: "0 2px 8px rgba(0,0,0,.3)" }, logo: { fontFamily: "Georgia, serif", color: "#C9A84C", fontSize: 20, fontWeight: "bold", letterSpacing: ".5px" }, sublogo: { color: "#9EB3CC", fontSize: 12, marginTop: 2 }, nav: { display: "flex", gap: 4 }, navBtn: (active) => ({ background: active ? "#C9A84C" : "transparent", color: active ? "#1B2A4A" : "#9EB3CC", border: "none", padding: "6px 14px", borderRadius: 6, cursor: "pointer", fontSize: 13, fontWeight: active ? "700" : "500", transition: "all .2s" }), main: { maxWidth: 1200, margin: "0 auto", padding: 24 }, grid2: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }, grid4: { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 16, marginBottom: 24 }, card: { background: "#fff", borderRadius: 12, padding: 20, boxShadow: "0 1px 4px rgba(0,0,0,.08)" }, statCard: (accent) => ({ background: "#1B2A4A", borderRadius: 12, padding: 20, borderLeft: `4px solid ${accent}` }), statNum: { fontSize: 32, fontWeight: "800", fontFamily: "Georgia, serif", color: "#C9A84C" }, statLabel: { fontSize: 12, color: "#9EB3CC", marginTop: 2, textTransform: "uppercase", letterSpacing: ".5px" }, sectionTitle: { fontFamily: "Georgia, serif", fontSize: 18, fontWeight: "700", color: "#1B2A4A", marginBottom: 16 }, badge: (color) => ({ background: color + "22", color, fontSize: 11, fontWeight: "700", padding: "2px 8px", borderRadius: 20, border: `1px solid ${color}44` }), btn: (variant = "primary") => ({ background: variant === "primary" ? "#C9A84C" : variant === "danger" ? "#C0392B" : "#E2DDD4", color: variant === "primary" ? "#1B2A4A" : variant === "danger" ? "#fff" : "#1B2A4A", border: "none", padding: "8px 18px", borderRadius: 8, cursor: "pointer", fontSize: 13, fontWeight: "700" }), input: { border: "1px solid #D8D3C8", borderRadius: 8, padding: "8px 12px", fontSize: 13, width: "100%", background: "#FAF8F4", color: "#1B2A4A", boxSizing: "border-box" }, select: { border: "1px solid #D8D3C8", borderRadius: 8, padding: "8px 12px", fontSize: 13, background: "#FAF8F4", color: "#1B2A4A" }, modal: { position: "fixed", inset: 0, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 100 }, modalBox: { background: "#fff", borderRadius: 16, padding: 28, width: 440, boxShadow: "0 8px 40px rgba(0,0,0,.2)" }, label: { fontSize: 12, fontWeight: "700", color: "#4A5568", marginBottom: 4, display: "block", textTransform: "uppercase", letterSpacing: ".4px" }, row: { display: "flex", gap: 12, alignItems: "center" }, churchRow: { display: "flex", alignItems: "center", gap: 14, padding: "12px 0", borderBottom: "1px solid #F0EDE6", cursor: "pointer" }, tag: { background: "#1B2A4A", color: "#C9A84C", fontSize: 10, fontWeight: "700", padding: "2px 7px", borderRadius: 20 }, }; // ── Dashboard ───────────────────────────────────────────────────────── const DashboardTab = () => (
{d.month}
))}Association Leadership Dashboard
Oversee. Connect. Strengthen. - {new Date().toLocaleDateString("en-US", { weekday:"long", year:"numeric", month:"long", day:"numeric" })}
{[ { label: "Member Churches", value: churches.length, accent: "#C9A84C" }, { label: "Disengaged Churches", value: disengaged, accent: "#C0392B" }, { label: "Total Giving (YTD)", value: fmt(totalGiving), accent: "#6B8F71" }, { label: "Pending Outreach", value: pendingTouchpoints, accent: "#5B8FCC" }, ].map((s, i) => (
))}
{s.value}
{s.label}
{/* Engagement overview */}
{/* Giving trend + Priority actions */}
{touchpoints.filter(t=>!t.completed).slice(0,4).map(tp => { const ch = churches.find(c=>c.id===tp.churchId); return (
); })}
Church Engagement Health
{[...churches].sort((a,b)=>a.engagementScore-b.engagementScore).map(c => (
{scoreLabel(c.engagementScore)}
))}
{c.name}
{daysSince(c.lastContact)} days since contact · {c.city}
Monthly Giving Trend
This month {fmt(thisMonthGiving)}
Upcoming Outreach
{ch?.name}
{tp.type} · {fmtDate(tp.date)}
{tp.description}
Member Church Directory
setChurchSearch(e.target.value)} />
setSelectedChurch(c)}>
))}{c.name} {c.tags.map(t => {t})}
{c.pastor} · {c.city} · {c.members} members
{c.notes &&
"{c.notes.slice(0,80)}{c.notes.length > 80 ? "…" : ""}"
}{scoreLabel(c.engagementScore)}
Last contact {daysSince(c.lastContact)}d ago
{c.email}
Engagement Strategy & Outreach Planner
? Priority - Disengaged ({disengagedChurches.length})
These churches have had minimal contact and need immediate personal outreach - phone call or in-person visit from the associational director.
{disengagedChurches.map(c => ({c.name} - {c.pastor}
{c.notes}
? Nurture - Needs Attention ({needsAttn.length})
Engaged but inconsistent. Invite to association events, offer training, check in regularly.
{needsAttn.map(c => ({c.name} - {c.pastor}
{c.notes}
? Healthy - Celebrate & Deploy ({healthy.length})
These churches are strong partners. Celebrate them, involve them in mentoring weaker churches, and invite them to lead in the association.
{healthy.map(c => ({c.name} - {c.pastor}
Partner ChurchAll Scheduled Touchpoints
{touchpoints.length === 0 &&
No touchpoints scheduled yet.
} {[...touchpoints].sort((a,b)=>a.completed-b.completed).map(tp => { const ch = churches.find(c=>c.id===tp.churchId); return ({ch?.name}
{tp.type} · {fmtDate(tp.date)}
{tp.description}
Giving Records
{[ { label: "Total YTD", value: fmt(totalGiving), color: "#6B8F71" }, { label: "This Month", value: fmt(thisMonthGiving), color: "#C9A84C" }, { label: "Contributing Churches", value: new Set(giving.map(g=>g.churchId)).size + " of " + churches.length, color: "#5B8FCC" }, ].map((s,i) => (
))}
{s.value}
{s.label}
Giving by Church (YTD)
{givingByChurch.map(([name, total]) => (
{name}
Transaction Log
{[...filteredGiving].sort((a,b)=>b.date.localeCompare(a.date)).map(g => (
))}
{g.churchName} {fmt(g.amount)}
{fmtDate(g.date)} · {g.category}
{g.note &&
{g.note}
}setSelectedChurch(null)}>
); }; // ── TABS ───────────────────────────────────────────────────────────────── const tabs = [ { id: "dashboard", label: "Dashboard" }, { id: "churches", label: "Churches" }, { id: "strategy", label: "Strategy" }, { id: "giving", label: "Giving" }, ]; return (
e.stopPropagation()}>
{c.name}
{c.pastor} · {c.city}
{[["Members", c.members],["Status", c.status],["Phone", c.phone],["Email", c.email]].map(([k,v])=>(
{k}
))}{v}
Notes
{c.notes || "No notes."}
YTD Giving
{fmt(churchTotal)}
{churchGiving.slice(0,3).map(g=>
{fmtDate(g.date)} · {g.category} · {fmt(g.amount)}
)}✝ Baptist Association
Leadership Portal
{tab === "dashboard" && } {tab === "churches" && } {tab === "strategy" && } {tab === "giving" && }
{/* Church detail */} {selectedChurch && } {/* Add Giving Modal */} {showAddGiving && (
Record Giving
{[ ["Church", ], ["Date", setNewGiving(g=>({...g,date:e.target.value}))} />], ["Amount ($)", setNewGiving(g=>({...g,amount:e.target.value}))} />], ["Category", ], ["Note (optional)", setNewGiving(g=>({...g,note:e.target.value}))} />], ].map(([label, input]) => (
{input}
))}
Schedule Touchpoint
{[ ["Church", ], ["Type", ], ["Date", setNewTouch(t=>({...t,date:e.target.value}))} />], ["Description", setNewTouch(t=>({...t,description:e.target.value}))} />], ].map(([label, input]) => (
{input}
))}
Add Member Church
{[ ["Church Name", "name", "text", "e.g. Fellowship Baptist Church"], ["Pastor", "pastor", "text", "Rev. First Last"], ["Members", "members", "number", "Approximate count"], ["City", "city", "text", ""], ["Phone", "phone", "tel", ""], ["Email", "email", "email", ""], ].map(([label, field, type, ph]) => (
setNewChurch(c=>({...c,[field]:e.target.value}))} />
))}
