/* ==========================================================================
DATENBANK: geschuetzte, rein lesende Katalogansicht.
Die vier Kataloge bleiben fachlich getrennt und werden erst hier unter einem
Premium-Zugang zusammengefuehrt. Uebungen kommen aus dem bestehenden
ExerciseCatalog; die grossen Lebensmittel-, Evidenz- und Molekuellisten
werden serverseitig gefiltert und in kleinen Fenstern nachgeladen.
========================================================================== */
const DB_BEREICHE = [
{
id: "uebungen", icon: "◫", label: "Übungen",
blurb: "Körperpartien, Geräte, Muskelziele und Anleitungen.",
},
{
id: "nahrungsmittel", icon: "▤", label: "Nahrungsmittel",
blurb: "Makros und Energie pro 100 g aus der Lebensmittelbasis.",
},
{
id: "evidenz", icon: "⌁", label: "Evidenz",
blurb: "Quellen und Einzelnachweise hinter den Wissensartikeln.",
},
{
id: "molekuele", icon: "◈", label: "Moleküle",
blurb: "Substanzen, Metaboliten und Zielproteine aus der Molekülkarte.",
},
];
const DB_FOOD_GROUP_LABELS = {
fleisch: "Fleisch", fisch: "Fisch", ei: "Eier", milch: "Milchprodukte",
huelsenfrucht: "Hülsenfrüchte", getreide: "Getreide und Beilagen",
gemuese: "Gemüse", obst: "Obst", nuss: "Nüsse und Samen", fett: "Fette und Öle",
suess: "Süßes", sonstiges: "Sonstiges", markt: "Markt",
};
const DB_MOLECULE_TYPES = [
["substance", "Substanz"], ["metabolite", "Metabolit"],
["receptor", "Rezeptor"], ["enzyme", "Enzym"],
["transporter", "Transporter"], ["regulator", "Regulator"],
["endpoint", "Analyse-Endpunkt"],
];
function datenbankDarf(user) {
return !!user && (user.plan === "premium" || user.role === "admin" || user.role === "mod");
}
function datenbankBereich(value) {
return DB_BEREICHE.some((bereich) => bereich.id === value) ? value : "uebungen";
}
function datenbankHref(id) {
return "/datenbank?bereich=" + encodeURIComponent(id);
}
function datenbankZahl(value, suffix = "") {
if (value === null || value === undefined || value === "") return "–";
const number = Number(value);
if (!Number.isFinite(number)) return "–";
const digits = Number.isInteger(number) ? 0 : 2;
return new Intl.NumberFormat(BB.lang || "de", {
maximumFractionDigits: digits,
}).format(number) + suffix;
}
function DatenbankLanding({ user }) {
return (
{BB.t("Premium-Datenbank")}
{BB.t("Die Datenbank hinter deinen Entscheidungen.")}
{BB.t("Übungen, Nahrungsmittel, Evidenz und Moleküle – zentral gesammelt, nachvollziehbar und nur lesend.")}
{BB.t("Zugriff auf die Datenbank gehört zur Premium-Mitgliedschaft.")} {BB.t("Alle Einträge bleiben unverändert. Du kannst sie durchsuchen und die Quellen einsehen.")}
{DB_BEREICHE.map((bereich, index) => (
{bereich.icon}
{BB.t(bereich.label)}
))}
BB
{BB.t("Lesender Zugriff")}
{BB.t("Vier Kataloge, ein Einstieg.")}
{DB_BEREICHE.map((bereich) => (
{bereich.icon}
{BB.t(bereich.label)}
{BB.t(bereich.blurb)}
))}
);
}
function DatenbankNavigation({ active, overview }) {
const counts = {};
(overview && overview.sections || []).forEach((item) => { counts[item.id] = item.count; });
return (
{DB_BEREICHE.map((bereich) => (
{bereich.icon}
{BB.t(bereich.label)}
{counts[bereich.id] !== undefined && (
{BB.num(counts[bereich.id])}
)}
))}
);
}
function DatenbankFehler({ error, onRetry }) {
return (
{BB.t("Die Datenbank konnte nicht geladen werden.")}
{error || BB.t("Das hat nicht geklappt.")}
{onRetry &&
{BB.t("Neu laden")} }
);
}
function DatenbankExercises() {
const [data, setData] = useState(null);
const [error, setError] = useState("");
const [version, setVersion] = useState(0);
useEffect(() => {
let alive = true;
setData(null);
setError("");
BB.get("/datenbank/uebungen")
.then((payload) => { if (alive) setData(payload); })
.catch((err) => { if (alive) setError(err.message || BB.t("Das hat nicht geklappt.")); });
return () => { alive = false; };
}, [version]);
const labelFuer = useCallback((id) => {
const muskel = (data && data.muskeln || []).find((item) => item.id === id);
return muskel ? BB.t(muskel.label) : id;
}, [data]);
return (
{BB.t("Katalog")}
{BB.t("Übungen")}
{BB.t("Durchsuche Übungen nach Körperpartie und Gerät. Die Angaben stammen aus dem ExerciseDB-Katalog.")}
{BB.t("Nur lesen")}
{error && setVersion((n) => n + 1)} />}
{!data && !error &&
}
{data && (
)}
);
}
function DatenbankFood() {
const [suche, setSuche] = useState("");
const [gruppe, setGruppe] = useState("");
const [items, setItems] = useState([]);
const [gruppen, setGruppen] = useState([]);
const [quelle, setQuelle] = useState(null);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
const request = useRef(0);
useEffect(() => {
const id = ++request.current;
let alive = true;
setLoading(true);
setItems([]);
setError("");
const timer = setTimeout(() => {
BB.get("/datenbank/nahrungsmittel" + BB.qs({
q: suche.trim(), gruppe, land: BB.country, lang: BB.lang, limit: 60, offset: 0,
}))
.then((data) => {
if (!alive || id !== request.current) return;
setItems(data.items || []);
setGruppen(data.gruppen || []);
setQuelle(data.quelle || null);
setTotal(data.total || 0);
setLoading(false);
})
.catch((err) => {
if (!alive || id !== request.current) return;
setError(err.message || BB.t("Das hat nicht geklappt."));
setLoading(false);
});
}, 180);
return () => { alive = false; clearTimeout(timer); };
}, [suche, gruppe]);
function mehrLaden() {
if (loadingMore || items.length >= total) return;
setLoadingMore(true);
BB.get("/datenbank/nahrungsmittel" + BB.qs({
q: suche.trim(), gruppe, land: BB.country, lang: BB.lang,
limit: 60, offset: items.length,
}))
.then((data) => {
setItems((old) => old.concat(data.items || []));
setTotal(data.total || 0);
})
.catch((err) => setError(err.message || BB.t("Das hat nicht geklappt.")))
.finally(() => setLoadingMore(false));
}
return (
{BB.t("Katalog")}
{BB.t("Nahrungsmittel")}
{BB.t("Durchsuche die Lebensmittelbasis nach Namen, Gruppe und Nährwerten.")}
{BB.t("Nur lesen")}
setSuche(event.target.value)}
placeholder={BB.t("Suche in der Datenbank …")}
aria-label={BB.t("Nahrungsmittel suchen")}
/>
setGruppe(event.target.value)} aria-label={BB.t("Alle Gruppen")}>
{BB.t("Alle Gruppen")}
{gruppen.map((key) => {BB.t(DB_FOOD_GROUP_LABELS[key] || key)} )}
{BB.num(total)} {BB.t("Einträge")}
{quelle && {BB.t("Quelle")}: {quelle.name} }
{error &&
}
{loading ?
: items.length === 0 ? (
) : (
<>
{items.map((item) => (
{item.label}
{item.gruppe && {BB.t(DB_FOOD_GROUP_LABELS[item.gruppe] || item.gruppe)} }
{BB.t("Kalorien")} {datenbankZahl(item.kcal, " kcal")}
{BB.t("Eiweiß")} {datenbankZahl(item.protein, " g")}
{BB.t("Kohlenhydrate")} {datenbankZahl(item.kh, " g")}
{BB.t("Fett")} {datenbankZahl(item.fett, " g")}
{BB.t("Nährwerte je 100 g")} · {item.quelle || BB.t("Kuratierte Lebensmittelbasis")}
))}
{items.length < total && (
{loadingMore ? BB.t("Datenbank wird geladen …") : BB.t("Mehr laden")} · {BB.num(items.length)} / {BB.num(total)}
)}
>
)}
);
}
function DatenbankEvidence() {
const [suche, setSuche] = useState("");
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
const request = useRef(0);
useEffect(() => {
const id = ++request.current;
let alive = true;
setLoading(true);
setItems([]);
setError("");
const timer = setTimeout(() => {
BB.get("/datenbank/evidenz" + BB.qs({ q: suche.trim(), limit: 50, offset: 0 }))
.then((data) => {
if (!alive || id !== request.current) return;
setItems(data.items || []);
setTotal(data.total || 0);
setLoading(false);
})
.catch((err) => {
if (!alive || id !== request.current) return;
setError(err.message || BB.t("Das hat nicht geklappt."));
setLoading(false);
});
}, 180);
return () => { alive = false; clearTimeout(timer); };
}, [suche]);
function mehrLaden() {
if (loadingMore || items.length >= total) return;
setLoadingMore(true);
BB.get("/datenbank/evidenz" + BB.qs({ q: suche.trim(), limit: 50, offset: items.length }))
.then((data) => {
setItems((old) => old.concat(data.items || []));
setTotal(data.total || 0);
})
.catch((err) => setError(err.message || BB.t("Das hat nicht geklappt.")))
.finally(() => setLoadingMore(false));
}
return (
{BB.t("Freigegebene Evidenz")}
{BB.t("Evidenz")}
{BB.t("Finde die Einzelnachweise hinter den freigegebenen Wissensartikeln.")}
{BB.t("Nur lesen")}
setSuche(event.target.value)}
placeholder={BB.t("Suche in der Datenbank …")}
aria-label={BB.t("Evidenz suchen")}
/>
{BB.num(total)} {BB.t("Nachweis")}
{error &&
}
{loading ?
: items.length === 0 ? (
) : (
<>
{items.map((item) => (
{item.kategorie || BB.t("Evidenz")}
{BB.t("Referenz")} {item.nummer}
{item.pmid && PMID {item.pmid} }
{item.referenz}
))}
{items.length < total && (
{loadingMore ? BB.t("Datenbank wird geladen …") : BB.t("Mehr laden")} · {BB.num(items.length)} / {BB.num(total)}
)}
>
)}
);
}
function DatenbankMolecules() {
const [suche, setSuche] = useState("");
const [typ, setTyp] = useState("");
const [items, setItems] = useState([]);
const [typen, setTypen] = useState(DB_MOLECULE_TYPES.map(([id, label]) => ({ id, label })));
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
const request = useRef(0);
useEffect(() => {
const id = ++request.current;
let alive = true;
setLoading(true);
setItems([]);
setError("");
const timer = setTimeout(() => {
BB.get("/datenbank/molekuele" + BB.qs({ q: suche.trim(), typ, limit: 60, offset: 0 }))
.then((data) => {
if (!alive || id !== request.current) return;
setItems(data.items || []);
setTypen(data.typen || typen);
setTotal(data.total || 0);
setLoading(false);
})
.catch((err) => {
if (!alive || id !== request.current) return;
setError(err.message || BB.t("Das hat nicht geklappt."));
setLoading(false);
});
}, 180);
return () => { alive = false; clearTimeout(timer); };
}, [suche, typ]);
function mehrLaden() {
if (loadingMore || items.length >= total) return;
setLoadingMore(true);
BB.get("/datenbank/molekuele" + BB.qs({ q: suche.trim(), typ, limit: 60, offset: items.length }))
.then((data) => {
setItems((old) => old.concat(data.items || []));
setTotal(data.total || 0);
})
.catch((err) => setError(err.message || BB.t("Das hat nicht geklappt.")))
.finally(() => setLoadingMore(false));
}
return (
{BB.t("Molekülkarte")}
{BB.t("Moleküle")}
{BB.t("Durchsuche die vorhandenen Moleküle, Metaboliten und Zielproteine.")}
{BB.t("Nur lesen")}
setSuche(event.target.value)}
placeholder={BB.t("Suche in der Datenbank …")}
aria-label={BB.t("Moleküle suchen")}
/>
setTyp(event.target.value)} aria-label={BB.t("Alle Typen")}>
{BB.t("Alle Typen")}
{typen.map((item) => {BB.t(item.label)} )}
{BB.num(total)} {BB.t("Einträge")}
{error &&
}
{loading ?
: items.length === 0 ? (
) : (
<>
{items.map((item) => (
{BB.t(item.typLabel || item.typ)}
{item.seed && {BB.t("Aus der Wissensbasis")} }
{item.label}
{item.name && item.name !== item.label && {item.name}
}
{item.pubchemCid &&
{BB.t("PubChem CID")} {item.pubchemCid} }
{item.uniprot &&
{BB.t("UniProt")} {item.uniprot} }
{item.artikelSlug && {BB.t("Zum Wissensartikel")} ↗}
))}
{items.length < total && (
{loadingMore ? BB.t("Datenbank wird geladen …") : BB.t("Mehr laden")} · {BB.num(items.length)} / {BB.num(total)}
)}
>
)}
);
}
function DatenbankBereich({ id }) {
if (id === "nahrungsmittel") return ;
if (id === "evidenz") return ;
if (id === "molekuele") return ;
return ;
}
function DatenbankPage({ params, legacy = false }) {
const { user, ready } = useAuth();
const requested = params && (params.get("bereich") || params.get("section"));
const active = datenbankBereich(requested || (legacy ? "uebungen" : "uebungen"));
const darf = datenbankDarf(user);
const [overview, setOverview] = useState(null);
const [error, setError] = useState("");
const [overviewVersion, setOverviewVersion] = useState(0);
useEffect(() => {
if (!darf) return undefined;
let alive = true;
setOverview(null);
setError("");
BB.get("/datenbank")
.then((data) => { if (alive) setOverview(data); })
.catch((err) => { if (alive) setError(err.message || BB.t("Das hat nicht geklappt.")); });
return () => { alive = false; };
}, [darf, overviewVersion]);
if (!ready) return
;
if (!darf) return ;
return (
{error
?
setOverviewVersion((n) => n + 1)} />
: }
);
}
Object.assign(window, {
DatenbankPage, DatenbankLanding, DatenbankNavigation,
});