App

    Nam felis tellus, molestie in congue sit amet, luctus ac purus. Nulla vitae semper nisi, quis commodo ante. Donec vitae leo sed sapien vestibulum pretium at quis neque. Proin dapibus semper suscipit. Nunc condimentum feugiat blandit. Mauris tincidunt nunc vel lectus tempus maximus. Integer vitae felis ac neque viverra molestie. Suspendisse rhoncus sollicitudin augue vel scelerisque. Integer blandit tortor sit amet varius congue. Quisque finibus turpis libero, consequat hendrerit mi posuere id. Nam tincidunt vel orci in porttitor. In id arcu sit amet nibh suscipit cursus nec vel turpis. Aliquam tincidunt lacus vel viverra pretium. Nulla porta pulvinar augue, vel commodo metus scelerisque a. Sed ornare diam in efficitur molestie. Interdum et malesuada fames ac ante ipsum primis in faucibus.

    Read More

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