/* ========================================================================== POST: reader for both curated wiki entries and community posts. ========================================================================== */ /* --- cross-link peek ----------------------------------------------------- */ function peekStyle(at) { return { left: Math.min(at.x, window.innerWidth - 316), top: at.y + 10 > window.innerHeight - 120 ? at.y - 110 : at.y + 10, }; } function Peek({ at, entry }) { return (
{entry.title}
{(entry.summary || "").slice(0, 150)}
); } /** Quellenangabe beim Überfahren eines Belegs im Fließtext. */ function SourcePeek({ at, source }) { return (
{BB.t("Quelle")} [{source.n}]
{source.text}
); } /* --- sources ------------------------------------------------------------- */ /** Target of a source entry: PubMed first, then DOI, then a plain address. */ function refHref(r) { if (r.pmid) return "https://pubmed.ncbi.nlm.nih.gov/" + encodeURIComponent(r.pmid) + "/"; if (r.doi) return "https://doi.org/" + String(r.doi).replace(/^(https?:\/\/)?(dx\.)?doi\.org\//i, ""); if (r.url && /^https?:\/\//i.test(r.url)) return r.url; return null; } function RefList({ refs }) { return (
{refs.map((r, i) => { const href = refHref(r); const badge = r.pmid ? "PMID " + r.pmid : r.doi ? "DOI" : BB.t("Quelle öffnen"); return (
[{r.n || i + 1}] {href ? ( {r.text} {badge} ) : ( {r.text} )}
); })}
); } /** Renders post body and wires up the auto-generated cross references. */ function Body({ post, index, refs, showTodo }) { const ref = useRef(null); const { go } = useRouter(); const [peek, setPeek] = useState(null); const [cited, setCited] = useState(null); const html = useMemo(() => { const raw = post.format === "html" ? post.body : BB.markdown(post.body, { trustedFigures: post.kind === "wiki" }); return post.kind === "wiki" ? BB.autoLink(raw, index, post.slug) : raw; }, [post.body, post.format, post.kind, post.slug, index]); // Querverweise setzt nur die Art „wiki“: der Index enthält Stoff- und // Themennamen, in einem Rezepttext hätte er nichts zu suchen. // Beleg im Text: die Nummer steht als data-ref am leeren , die // Bedienbarkeit kommt von hier, damit das Artikel-HTML sauber bleibt. const sourceOf = (el) => (refs || []).find((r, i) => String(r.n || i + 1) === el.dataset.ref); const jumpToSource = (el) => { const row = document.getElementById("quelle-" + el.dataset.ref); if (!row) return; const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches; row.scrollIntoView({ behavior: still ? "auto" : "smooth", block: "center" }); row.classList.add("hit"); setTimeout(() => row.classList.remove("hit"), 2500); }; useEffect(() => { const root = ref.current; if (!root) return; const find = (e) => e.target.closest && e.target.closest(".auto-link"); const findCite = (e) => e.target.closest && e.target.closest(".cite"); root.querySelectorAll(".cite").forEach((el) => { const src = sourceOf(el); el.setAttribute("tabindex", "0"); el.setAttribute("role", "button"); el.setAttribute("aria-label", BB.t("Quelle") + " " + el.dataset.ref + (src ? ": " + src.text : "")); }); const onClick = (e) => { const c = findCite(e); if (c) { e.preventDefault(); setCited(null); jumpToSource(c); return; } const a = find(e); if (!a) return; e.preventDefault(); setPeek(null); go("/wissen/" + a.dataset.slug); }; const onKey = (e) => { if (e.key !== "Enter" && e.key !== " ") return; const c = findCite(e); if (!c) return; e.preventDefault(); jumpToSource(c); }; const onOver = (e) => { const c = findCite(e); if (c) { const src = sourceOf(c); if (!src) return; const r = c.getBoundingClientRect(); setCited({ at: { x: r.left, y: r.bottom }, source: { ...src, n: c.dataset.ref } }); return; } const a = find(e); if (!a) return; const entry = index.find((x) => x.slug === a.dataset.slug); if (!entry) return; const r = a.getBoundingClientRect(); setPeek({ at: { x: r.left, y: r.bottom }, entry }); }; const onOut = (e) => { if (findCite(e)) setCited(null); if (find(e)) setPeek(null); }; root.addEventListener("click", onClick); root.addEventListener("keydown", onKey); root.addEventListener("mouseover", onOver); root.addEventListener("mouseout", onOut); return () => { root.removeEventListener("click", onClick); root.removeEventListener("keydown", onKey); root.removeEventListener("mouseover", onOver); root.removeEventListener("mouseout", onOut); }; }, [html, index, go, refs]); return ( <> {/* show-todo blendet die Bildplatzhalter ein, die dem Leser verborgen bleiben */}
{peek && } {cited && } ); } /* --- kind-specific side panels ------------------------------------------- */ function WikiPanel({ post, categories }) { const d = post.data || {}; const schema = BB.schemaFor(categories, post.categoryId); const cat = BB.catById(categories, post.categoryId); return ( <> {(d.molecule || d.moleculeNote) && (
{BB.t("Strukturformel")}
{d.molecule && d.formula && (
{d.formula}
)}
)} {schema && d.bars && (
{BB.t("Kennzahlen")}
)}
{BB.t("Steckbrief")}
{cat && <>
{BB.t("Kategorie")}
{cat.label}
} {d.formula && <>
{BB.t("Formel")}
{d.formula}
} {d.aliases && d.aliases.length > 0 && <>
{BB.t("Auch bekannt als")}
{d.aliases.join(", ")}
}
{BB.t("Aufrufe")}
{BB.num(post.views)}
); } function StackPanel({ post }) { const d = post.data || {}; const compounds = Array.isArray(d.compounds) ? d.compounds.filter((c) => c && c.name) : []; if (!compounds.length && !d.weeks && !d.goal) return null; return (
{BB.t("Protokoll")}
{d.goal && <>
{BB.t("Ziel")}
{d.goal}
} {d.weeks && <>
{BB.t("Dauer")}
{d.weeks} Wochen
} {d.experience && <>
{BB.t("Erfahrung")}
{d.experience}
}
{compounds.length > 0 && ( <>
{BB.t("Wirkstoffe")}
{compounds.map((c, i) => (
{c.name} {[c.dose, c.freq].filter(Boolean).join(" · ")}
))}
)}
); } function TransformationPanel({ post }) { const d = post.data || {}; const pairs = [ ["Gewicht", d.weightBefore, d.weightAfter, "kg"], ["Körperfett", d.bfBefore, d.bfAfter, "%"], ].filter(([, a, b]) => a || b); if (!pairs.length && !d.months && !d.natural) return null; return (
{BB.t("Zahlen")}
{pairs.map(([label, a, b, unit]) => (
{label}
{a || "k. A."}{unit} → {b || "k. A."}{unit}
))} {d.months && <>
{BB.t("Zeitraum")}
{d.months} Monate
} {d.natural !== undefined && <>
{BB.t("Natural")}
{d.natural ? "Ja" : "Nein"}
}
{d.beforeUrl && Vorher} {d.afterUrl && Nachher}
); } function RecipePanel({ post }) { const d = post.data || {}; const ingredients = Array.isArray(d.ingredients) ? d.ingredients.filter((c) => c && c.name) : []; const macros = [ ["kcal", BB.t("Kalorien"), "kcal"], ["protein", BB.t("Eiweiß"), "g"], ["carbs", BB.t("Kohlenhydrate"), "g"], ["fat", "Fett", "g"], ["fiber", BB.t("Ballaststoffe"), "g"], ].filter(([key]) => d[key]); if (!ingredients.length && !macros.length && !d.servings && !d.minutes) return null; return ( <>
{BB.t("Rezept")}
{d.servings && <>
{BB.t("Portionen")}
{d.servings}
} {d.minutes && <>
{BB.t("Zubereitungszeit")}
{d.minutes} {BB.t("Minuten")}
} {d.difficulty && <>
{BB.t("Aufwand")}
{d.difficulty}
}
{macros.length > 0 && ( <>
{BB.t("Nährwerte")} ({BB.t("pro Portion")})
{macros.map(([key, label, unit]) => (
{d[key]}{unit} {label}
))}
)}
{ingredients.length > 0 && (
{BB.t("Zutaten")}
    {ingredients.map((c, i) => (
  • {c.amount || ""} {c.name}
  • ))}
)} ); } /* --- Redaktionsstand ------------------------------------------------------ */ /** Markiert einen Beitrag als geprüft oder überarbeitungsbedürftig. Sichtbar nur für Admins und Mods, direkt beim Lesen. */ function ReviewBox({ post, onChange }) { const notify = useToast(); const [open, setOpen] = useState(false); const [note, setNote] = useState(post.reviewNote || ""); const [busy, setBusy] = useState(false); const state = post.review || ""; useEffect(() => { setNote(post.reviewNote || ""); }, [post.reviewNote]); async function mark(next, nextNote) { setBusy(true); try { const r = await BB.post("/admin/posts/" + post.id + "/review", { review: next, note: nextNote === undefined ? note : nextNote, }); onChange(r); notify(next ? "Markiert: " + BB.REVIEWS[next].label : "Markierung entfernt"); setOpen(false); } catch (err) { notify(err.message, "bad"); } finally { setBusy(false); } } return (
{BB.t("Redaktion")} {Object.entries(BB.REVIEWS).map(([key, r]) => ( ))} {post.reviewAt && ( {BB.when(post.reviewAt)} )}
{!open && post.reviewNote &&

{post.reviewNote}

} {open && (