/* P.E.S.T Web App — shared helpers, store context, common UI atoms */ const { useState, useEffect, useRef, useContext, useMemo, useCallback } = React; /* ---------- store context (provider lives in main.jsx) ---------- */ window.StoreCtx = window.StoreCtx || React.createContext(null); function useStore() { return useContext(window.StoreCtx); } /* ---------- toast (event-based, no context plumbing) ---------- */ function toast(msg, kind) { window.dispatchEvent(new CustomEvent("pest-toast", { detail: { msg, kind: kind || "ok" } })); } function ToastHost() { const [items, setItems] = useState([]); useEffect(() => { let id = 0; const on = (e) => { const t = { id: ++id, ...e.detail }; setItems((x) => [...x, t]); setTimeout(() => setItems((x) => x.filter((i) => i.id !== t.id)), 2600); }; window.addEventListener("pest-toast", on); return () => window.removeEventListener("pest-toast", on); }, []); return (
{items.map((t) => (
{t.kind === "err" ? : } {t.msg}
))}
); } /* ---------- deterministic color for initials logo tiles ---------- */ function hueFromString(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; return h; } // Resolve a stored logo path to an actual URL: // "projects/x.png" → nc-proxy.php?img=projects/x.png (admin-uploaded, served from Nextcloud) // "tokens/x.png" → nc-proxy.php?img=tokens/x.png // "assets/..." → as-is (static, served alongside the app) // "http(s)://..." → as-is (external) function resolveLogo(logo) { if (!logo) return null; if (/^https?:\/\//i.test(logo)) return logo; if (logo.indexOf("projects/") === 0 || logo.indexOf("tokens/") === 0) { return "nc-proxy.php?img=" + encodeURIComponent(logo); } return logo; } function LogoTile({ project, className }) { if (!project) return null; const src = resolveLogo(project.logo); if (src) { return (
{project.name}
); } const h = hueFromString(project.name); const bg = `linear-gradient(150deg, hsl(${h} 70% 26%), hsl(${(h + 40) % 360} 65% 16%))`; const initials = project.name.replace(/[^A-Za-z0-9 ]/g, "").split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase(); return (
{initials}
); } /* ---------- time ago ---------- */ function timeAgo(ts) { const d = Math.floor((Date.now() - ts) / 86400000); if (d <= 0) return "today"; if (d === 1) return "yesterday"; if (d < 7) return d + "d ago"; if (d < 30) return Math.floor(d / 7) + "w ago"; return Math.floor(d / 30) + "mo ago"; } /* ---------- member helpers ---------- */ const BADGE_META = { leader: { full: "Leader", short: "Leader", cls: "lead" }, "co-leader": { full: "Co-Leader", short: "Co-Lead", cls: "co" }, "genetic-coder": { full: "Genetic Coder", short: "Coder", cls: "gcoder" }, oracle: { full: "Oracle", short: "Oracle", cls: "oracle" }, }; function memberBadges(m) { const b = []; if (m.role === "leader") b.push("leader"); if (m.role === "co-leader") b.push("co-leader"); (m.tags || []).forEach((t) => BADGE_META[t] && b.push(t)); return b; } function roleLabel(m) { if (m.role === "leader") return "Leader"; if (m.role === "co-leader") return "Co-Leader"; if ((m.tags || []).includes("oracle")) return "Oracle"; if ((m.tags || []).includes("genetic-coder")) return "Genetic Coder"; return "Core-Member"; } function sortPriority(m) { if (m.role === "leader") return 0; if (m.role === "co-leader") return 1; if ((m.tags || []).includes("genetic-coder")) return 2; if ((m.tags || []).includes("oracle")) return 3; return 4; } /* honorary badges (additional to role) */ const HBADGE_ORDER = ["genius", "supervisor", "curator", "eventhost", "moderator"]; const HBADGE = { genius: { label: "Genius", tip: "The brain behind every calculation", icon: window.AIcon.genius, cls: "hb-genius" }, supervisor: { label: "Supervisor", tip: "Keeps watch over the hive", icon: window.AIcon.supervisor, cls: "hb-supervisor" }, curator: { label: "Content Curator", tip: "Content Curator", icon: window.AIcon.curator, cls: "hb-curator" }, eventhost: { label: "Event Host", tip: "Event Host", icon: window.AIcon.eventhost, cls: "hb-eventhost" }, moderator: { label: "Moderator", tip: "Moderator", icon: window.AIcon.moderator, cls: "hb-moderator" }, }; function honoraryBadges(m) { return HBADGE_ORDER.filter((k) => (m.badges || []).includes(k)); } function badgePriority(m) { const b = honoraryBadges(m); return b.length ? HBADGE_ORDER.indexOf(b[0]) : 99; } function sortMembers(list) { return list.map((m, i) => [m, i]).sort((a, b) => sortPriority(a[0]) - sortPriority(b[0]) || badgePriority(a[0]) - badgePriority(b[0]) || a[1] - b[1]).map((x) => x[0]); } function memberInitials(m) { return m.nick.slice(0, 2); } /* honorary medals overlay (on PFP edge) */ function HMedals({ m, big }) { const hb = honoraryBadges(m); if (!hb.length) return null; return (
{hb.map((k) => { const B = HBADGE[k]; const I = B.icon; return ; })}
); } /* member avatar */ function MAvatar({ m, idx, className, withMedals }) { const pfp = (m.pfps && m.pfps[idx || 0]) || (m.pfps && m.pfps[0]); const img = pfp && pfp.img; return (
{img ? {m.nick} :
{memberInitials(m)}
} {withMedals && }
); } Object.assign(window, { useStore, toast, ToastHost, LogoTile, resolveLogo, hueFromString, timeAgo, BADGE_META, memberBadges, roleLabel, sortMembers, sortPriority, MAvatar, memberInitials, HBADGE, HBADGE_ORDER, honoraryBadges, badgePriority, HMedals, });