/* ========================================================================== BROWSE: /wissen (curated knowledge base) and /forum (community feed). ========================================================================== */ const PAGE = 24; /** Shared fetching + pagination for both views. */ function usePosts(filters) { const [state, setState] = useState({ items: [], total: 0, loading: true }); const [offset, setOffset] = useState(0); const key = JSON.stringify(filters); useEffect(() => { setOffset(0); }, [key]); useEffect(() => { let alive = true; setState((s) => ({ ...s, loading: true })); BB.get("/posts" + BB.qs(Object.assign({}, filters, { limit: PAGE, offset }))) .then((d) => { if (!alive) return; setState((s) => ({ items: offset === 0 ? d.items : s.items.concat(d.items), total: d.total, loading: false, })); }) .catch(() => { if (alive) setState({ items: [], total: 0, loading: false }); }); return () => { alive = false; }; }, [key, offset]); const more = state.items.length < state.total; return { ...state, more, loadMore: () => setOffset((o) => o + PAGE) }; } /* --- category tree ------------------------------------------------------- */ function TreeNode({ cat, cats, active, onPick, depth }) { const kids = BB.children(cats, cat.id); const inPath = useMemo( () => active === cat.id || BB.ancestors(cats, active).some((a) => a.id === cat.id), [active, cat.id, cats] ); const [open, setOpen] = useState(inPath); useEffect(() => { if (inPath) setOpen(true); }, [inPath]); return (
{open && kids.length > 0 && (
{kids.map((k) => ( ))}
)}
); } function CategoryTree({ cats, active, onPick }) { return ( ); } /* --- /wissen ------------------------------------------------------------- */ function Wissen({ categories, params }) { const { go } = useRouter(); const { user } = useAuth(); const cat = params.get("cat") || ""; const q = params.get("q") || ""; const onlyUnread = params.get("unread") === "1"; const [draft, setDraft] = useState(q); useEffect(() => { setDraft(q); }, [q]); const setParam = useCallback((patch, opts) => { const next = new URLSearchParams(params); Object.entries(patch).forEach(([k, v]) => (v ? next.set(k, v) : next.delete(k))); const s = next.toString(); go("/wissen/alle" + (s ? "?" + s : ""), opts); }, [go, params]); // Die Liste folgt dem Tippen mit kurzer Pause, damit nicht jeder einzelne // Tastendruck einen Request ausloest. Replace haelt die Browser-Historie bei // einem Suchbegriff sauber, waehrend Kategoriewechsel weiter zurueckgehen. useEffect(() => { const wert = draft.trim(); if (wert === q) return undefined; const timer = setTimeout(() => { setParam({ q: wert }, { keepScroll: true, replace: true }); }, 250); return () => clearTimeout(timer); }, [draft, q, setParam]); const { items, total, loading, more, loadMore } = usePosts({ collection: "wiki", category: cat, q, sort: q ? "new" : "title", unread: onlyUnread ? 1 : "", }); const current = BB.catById(categories, cat); const trail = cat ? BB.ancestors(categories, cat) : []; const forumTarget = current && current.forumOnly ? "/forum?kind=recipe" + (cat !== "rezepte" ? "&category=" + encodeURIComponent(cat) : "") : ""; useEffect(() => { if (forumTarget) go(forumTarget); }, [forumTarget, go]); // progress for the selected branch, or for the whole knowledge base const progress = useMemo(() => { const readableTotal = (entry) => BB.isAdmin(user) ? (entry.count || 0) : (entry.readyCount !== undefined ? entry.readyCount : (entry.publishedCount !== undefined ? entry.publishedCount : (entry.count || 0))); if (current) return { read: current.readCount || 0, total: readableTotal(current) }; return BB.children(categories, null).filter((c) => !c.forumOnly).reduce( (acc, c) => ({ read: acc.read + (c.readCount || 0), total: acc.total + readableTotal(c) }), { read: 0, total: 0 } ); }, [categories, current, user]); if (forumTarget) return
; return (
{BB.t("Wissensbasis")} {trail.map((a) => ( / {a.label} ))}

{current ? current.label : BB.t("Alles Wissen")}

{current && current.blurb ? current.blurb : BB.t("Redaktionell gepflegte Einträge: Mechanismus, Kennzahlen, Nebenwirkungen und Quellen.")}

{user && progress.total > 0 && (
)}
{ e.preventDefault(); setParam({ q: draft.trim() }, { keepScroll: true, replace: true }); }} > ⌕ setDraft(e.target.value)} aria-busy={loading} placeholder={BB.t("Stoff, Alias oder Inhalt …")} aria-label={BB.t("Wissensbasis durchsuchen")} />
{user && ( setParam({ unread: v === "unread" ? "1" : "" })} options={[{ value: "all", label: BB.t("Alle") }, { value: "unread", label: BB.t("Ungelesen") }]} /> )} {BB.num(total)} {BB.t("Einträge")}
{loading && items.length === 0 ? ( ) : items.length === 0 ? ( {BB.t("Für diese Kombination aus Kategorie und Suche gibt es keine Einträge.")} ) : (
{items.map((p) => )}
)} {more && (
)}
); } /* --- /forum -------------------------------------------------------------- */ const SORTS = [ { value: "new", label: "Neu" }, { value: "top", label: "Top" }, { value: "discussed", label: "Diskutiert" }, ]; function Forum({ params }) { const { go } = useRouter(); const { user } = useAuth(); const kind = params.get("kind") || ""; const category = params.get("category") || ""; const tag = params.get("tag") || ""; const sort = params.get("sort") || "new"; const q = params.get("q") || ""; const [draft, setDraft] = useState(q); useEffect(() => { setDraft(q); }, [q]); const setParam = (patch) => { const next = new URLSearchParams(params); Object.entries(patch).forEach(([k, v]) => (v ? next.set(k, v) : next.delete(k))); const s = next.toString(); go("/forum" + (s ? "?" + s : "")); }; const { items, total, loading, more, loadMore } = usePosts({ collection: "forum", kind, category, tag, sort, q, }); return (
{BB.t("Forum")}

{BB.t("Was die Community teilt")}

+ {BB.t("Beitrag schreiben")}

{BB.t("Artikel, Erfahrungsberichte, Stack-Protokolle, Rezepte und Transformationen aus Community und Redaktion.")}

setParam({ kind: v })} options={[{ value: "", label: "Alle" }].concat( BB.USER_KINDS.map((k) => ({ value: k, label: BB.KINDS[k].label })) )} />
setParam({ sort: v })} options={SORTS} />
{ e.preventDefault(); setParam({ q: draft }); }} > ⌕ setDraft(e.target.value)} placeholder={BB.t("Suchen …")} aria-label={BB.t("Forum durchsuchen")} />
{tag && (
{BB.t("Gefiltert nach")}
)} {loading && items.length === 0 ? ( ) : items.length === 0 ? ( {BB.t("Ersten Beitrag schreiben")} : {BB.t("Profil erstellen")} } > {BB.t("Hier ist noch nichts. Mit deinem Beitrag fängt es an.")} ) : ( <>
{BB.num(total)} {BB.t("Beiträge")}
{items.map((p) => )}
)} {more && (
)}
); } Object.assign(window, { Wissen, Forum, CategoryTree, usePosts });