import React, { useState, useEffect, useCallback } from "react"; /* ============================================================ ENCUENTRO DE HOMBRES — Guide Matching Board Visual language mirrors the hand-built slide: navy headers, steel-blue participant cards, 4-cell stat strip. ============================================================ */ const NAVY = "#16284A"; const CARD_BLUE = "#3E6BA6"; const CARD_BLUE_DK = "#345C92"; const PAPER = "#F4F6F9"; const INK = "#1E2733"; const GOLD = "#D9A441"; const RED = "#B14A3A"; const LANGS = ["ESP", "ENG", "BIL"]; const AREAS = ["N", "S", "E", "W", "NE", "NW", "SE", "SW"]; const STATUS_LABELS = { 1: "Unchurched", 2: "Attends New Life", 3: "Out of town, has church home", }; const DEFAULT_TAGS = [ "Immigrant", "Collegiate", "Cholo", "Veteran believer", "Young professional", "Recently out", ]; const uid = () => Math.random().toString(36).slice(2, 10); /* ---------- persistence (graceful: falls back to memory) ---------- */ async function loadState() { try { const res = await window.storage.get("encuentro-state-v1"); if (res && res.value) return JSON.parse(res.value); } catch (e) { /* first run or storage unavailable */ } return null; } async function saveState(state) { try { await window.storage.set("encuentro-state-v1", JSON.stringify(state)); } catch (e) { /* in-memory only; still works for the session */ } } /* ---------- matching ---------- */ function langCompatible(a, b) { if (!a || !b) return true; return a === "BIL" || b === "BIL" || a === b; } function scoreMatch(p, g, load, capacity) { if (load >= capacity) return null; // guide is full let total = 0, max = 0; const reasons = []; let warning = null; // 1. Age — top priority const pAge = parseInt(p.age, 10); const gAge = parseInt(g.age, 10); if (pAge && gAge) { max += 40; const diff = Math.abs(pAge - gAge); total += Math.max(0, 1 - diff / 20) * 40; if (diff <= 7) reasons.push("Close in age (" + gAge + " · " + pAge + ")"); } // 2. Language max += 30; if (langCompatible(p.lang, g.lang)) { total += 30; if (p.lang && g.lang) { reasons.push( p.lang === g.lang ? "Both " + (p.lang === "ESP" ? "Spanish" : p.lang === "ENG" ? "English" : "bilingual") : "Language works (bilingual)" ); } } else { warning = "Language mismatch — " + g.lang + " guide, " + p.lang + " participant"; } // 3. Profile const pt = p.tags || []; const gt = g.tags || []; if (pt.length && gt.length) { max += 20; const shared = pt.filter((t) => gt.includes(t)); if (shared.length) { total += 20; reasons.push("Shared profile: " + shared.join(", ")); } } // 4. Side of town if (p.area && g.area) { max += 10; if (p.area === g.area) { total += 10; reasons.push("Same side of town (" + p.area + ")"); } } let pct = max > 0 ? (total / max) * 100 : 50; pct -= load * 3; // gentle load balancing return { guideId: g.id, score: Math.round(Math.max(0, pct)), reasons, warning }; } function rankGuides(participant, guides, participants) { const loads = {}; guides.forEach((g) => (loads[g.id] = 0)); participants.forEach((p) => { if (p.guideId && loads[p.guideId] !== undefined && p.id !== participant.id) loads[p.guideId]++; }); return guides .map((g) => scoreMatch(participant, g, loads[g.id], g.capacity || 3)) .filter(Boolean) .sort((a, b) => b.score - a.score); } /* ---------- tiny UI atoms ---------- */ function Chip({ active, onClick, children, danger }) { return (
); } /* ---------- participant card (mirrors the slide) ---------- */ function PCard({ p, onClick, ghost }) { return (
{p.name}
setView(id)} style={{ flex: 1, padding: "12px 4px", background: view === id ? NAVY : "transparent", color: view === id ? "#fff" : NAVY, border: "none", borderBottom: view === id ? "3px solid " + GOLD : "3px solid transparent", fontWeight: 700, fontSize: 14, letterSpacing: "0.04em", textTransform: "uppercase", cursor: "pointer", fontFamily: "'Archivo', sans-serif", }} > {children}
); return (
Guide matching board · {guides.length} guides · {participants.length} participants
{/* tabs */}
Guides Check-in Board
{guides.length === 0 && !gForm && (
Set up your Guides first
setGForm({ name: "", age: "", lang: "", area: "", tags: [], capacity: 3 }) } style={{ background: NAVY, color: "#fff", border: "none", padding: "12px 26px", borderRadius: 8, fontWeight: 700, fontSize: 15, cursor: "pointer", fontFamily: "inherit", }} > + Add first Guide
)} {guides.length > 0 && !gForm && (
setGForm({ ...g })} style={{ background: "#fff", borderRadius: 10, border: "1px solid #D8DEE7", overflow: "hidden", cursor: "pointer", }} >
{(g.tags || []).join(" · ") || "No profile set"}
))}
setGForm({ name: "", age: "", lang: "", area: "", tags: [], capacity: 3 }) } style={{ marginTop: 16, background: "#fff", color: NAVY, border: "1.5px solid " + NAVY, padding: "11px 22px", borderRadius: 8, fontWeight: 700, fontSize: 14.5, cursor: "pointer", fontFamily: "inherit", }} > + Add Guide
)} {gForm && (
{gForm.id ? "Edit Guide" : "New Guide"}
setGForm({ ...gForm, name: e.target.value })} placeholder="Guide's name" />
setGForm({ ...gForm, age: e.target.value.replace(/\D/g, "") }) } placeholder="00" />
setGForm({ ...gForm, lang: l })} > {l} ))}
setGForm({ ...gForm, area: gForm.area === a ? "" : a })} > {a} ))}
setGForm({ ...gForm, tags: (gForm.tags || []).includes(t) ? gForm.tags.filter((x) => x !== t) : [...(gForm.tags || []), t], }) } > {t} ))}
setGForm({ ...gForm, capacity: n })} > {n} ))}
setGForm(null)} style={{ background: "transparent", color: "#5A6678", border: "none", padding: "12px 16px", fontWeight: 600, fontSize: 14.5, cursor: "pointer", fontFamily: "inherit", }} > Cancel {gForm.id && (
deleteGuide(gForm.id)} style={{ marginLeft: "auto", background: "transparent", color: RED, border: "none", padding: "12px 8px", fontWeight: 600, fontSize: 14, cursor: "pointer", fontFamily: "inherit", }} > Delete
)}
)} )} {/* ================= CHECK-IN ================= */} {view === "checkin" && (
Add your Guides first, then check participants in here.
) : !suggestions ? (
Participant check-in
setForm({ ...form, name: e.target.value })} placeholder="Participant's name" />
setForm({ ...form, age: e.target.value.replace(/\D/g, "") }) } placeholder="00" />
setForm({ ...form, lang: l })} > {l} ))}
setForm({ ...form, area: form.area === a ? "" : a })} > {a} ))}
setForm({ ...form, status: form.status === n ? "" : n })} > {n} — {STATUS_LABELS[n]} ))}
setForm({ ...form, tags: form.tags.includes(t) ? form.tags.filter((x) => x !== t) : [...form.tags, t], }) } > {t} ))}
setNewTag(e.target.value)} placeholder="Add a new profile type…" style={{ fontSize: 13.5, padding: "8px 10px" }} onKeyDown={(e) => { if (e.key === "Enter" && newTag.trim()) { const t = newTag.trim(); if (!tagOptions.includes(t)) setTagOptions([...tagOptions, t]); setForm({ ...form, tags: [...form.tags, t] }); setNewTag(""); } }} />
{ const t = newTag.trim(); if (!t) return; if (!tagOptions.includes(t)) setTagOptions([...tagOptions, t]); setForm({ ...form, tags: [...form.tags, t] }); setNewTag(""); }} style={{ background: "#fff", border: "1.5px solid #C6CEDA", borderRadius: 8, padding: "0 14px", fontWeight: 700, cursor: "pointer", color: NAVY, fontFamily: "inherit", }} > Add
All Guides are at capacity. You can add this participant unassigned and rearrange the board.
)} {suggestions.map((s, i) => { const g = guides.find((x) => x.id === s.guideId); return (
{g.age || "—"} · {g.lang || "—"} · {g.area || "—"} ·{" "} {guideLoad(g.id)}/{g.capacity || 3}
{s.warning && (
{s.score}
assign(s.guideId)} style={{ background: NAVY, color: "#fff", border: "none", padding: "10px 18px", borderRadius: 8, fontWeight: 700, fontSize: 14, cursor: "pointer", fontFamily: "inherit", }} > Assign
); })}
{ setSuggestions(null); setPendingP(null); }} style={{ background: "transparent", color: "#5A6678", border: "none", padding: "11px 12px", fontWeight: 600, fontSize: 14, cursor: "pointer", fontFamily: "inherit", }} > ← Edit info
)} )} {/* ================= BOARD ================= */} {view === "board" && (
{guides.length === 0 ? (
{guides.map((g) => { const kids = participants.filter((p) => p.guideId === g.id); return (
{kids.length}/{g.capacity || 3}
setMoveTarget(p)} /> ))} {kids.length === 0 && (
{unassigned.map((p) => (
setMoveTarget(p)} /> ))}
)}
window.print()} style={{ background: "#fff", color: NAVY, border: "1.5px solid " + NAVY, padding: "10px 18px", borderRadius: 8, fontWeight: 700, fontSize: 13.5, cursor: "pointer", fontFamily: "inherit", }} > Print board {participants.length > 0 && (
setMoveTarget(null)} style={{ position: "fixed", inset: 0, background: "rgba(22,40,74,0.55)", display: "flex", alignItems: "flex-end", justifyContent: "center", zIndex: 50, }} >
e.stopPropagation()} style={{ background: "#fff", borderRadius: "14px 14px 0 0", padding: "20px 18px 28px", width: "100%", maxWidth: 480, maxHeight: "75vh", overflowY: "auto", }} >
Move to a different Guide:
{rankGuides(moveTarget, guides, participants) .concat( guides .filter( (g) => guideLoad(g.id) - (moveTarget.guideId === g.id ? 1 : 0) >= (g.capacity || 3) ) .map((g) => ({ guideId: g.id, score: null, reasons: ["Full"], warning: null })) ) .filter((s) => s.guideId !== moveTarget.guideId) .map((s) => { const g = guides.find((x) => x.id === s.guideId); const full = s.score === null; return (
moveParticipant(moveTarget.id, s.guideId)} style={{ display: "flex", width: "100%", alignItems: "center", gap: 10, background: full ? "#F0F2F6" : "#fff", border: "1px solid #D8DEE7", borderRadius: 8, padding: "11px 13px", marginBottom: 8, cursor: full ? "default" : "pointer", textAlign: "left", fontFamily: "inherit", color: full ? "#8B94A3" : INK, }} > {s.reasons.slice(0, 2).join(" · ")} {s.warning ? " · ⚠ " + s.warning : ""} {s.score !== null && ( {moveTarget.guideId && (
moveParticipant(moveTarget.id, null)} style={{ background: "#fff", color: NAVY, border: "1.5px solid #C6CEDA", padding: "10px 16px", borderRadius: 8, fontWeight: 700, fontSize: 13.5, cursor: "pointer", fontFamily: "inherit", }} > Unassign
)}
removeParticipant(moveTarget.id)} style={{ background: "transparent", color: RED, border: "none", padding: "10px 10px", fontWeight: 600, fontSize: 13.5, cursor: "pointer", fontFamily: "inherit", }} > Remove participant
setMoveTarget(null)} style={{ marginLeft: "auto", background: "transparent", color: "#5A6678", border: "none", padding: "10px 10px", fontWeight: 600, fontSize: 13.5, cursor: "pointer", fontFamily: "inherit", }} > Close
)}
); }