/* 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 (
);
}
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 ?

:
{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,
});