{/* **Ausserhalb der Kopfzeile, sonst faellt es nach dem Scrollen zusammen.**
`.hdr.stuck` traegt `backdrop-filter`, und ein Filter macht das
Element zum Bezugsrahmen fuer `position: fixed` darin -- dieselbe
Falle wie beim Vollbild der Molekuelkarte, nur eine Ebene hoeher. Am
Seitenanfang stand die Klasse noch nicht, weiter unten schon: das
Menue klappte dann auf die 76 Pixel der Leiste zusammen und war
unsichtbar. Als Portal an `body` haengt es an keinem Vorfahren mehr. */}
{menu && ReactDOM.createPortal((
), document.body)}
);
}
/* --- footer -------------------------------------------------------------- */
function Footer({ categories }) {
const tops = BB.children(categories, null).filter((c) => !c.forumOnly).slice(0, 6);
return (
);
}
/* --- noch gesperrte Bereiche --------------------------------------------- */
/* Forum und Registrierung sind waehrend des Aufbaus zu. Die Sperre gilt im
Server, hier steht nur die Erklaerung: wer den Bereich aufruft, soll wissen
warum er leer ist, statt in einen 403 zu laufen. */
function GesperrtPage({ title, message }) {
return (
{BB.t("Zur Wissensbasis")}}
>
{BB.t(message)}
);
}
/* --- routing ------------------------------------------------------------- */
function Routes({ categories, stats, onReloadCategories }) {
const { path } = useRouter();
const { user } = useAuth();
const isEditor = !!user && (user.role === "admin" || user.role === "mod");
const [pathname, search] = path.split("?");
const params = useMemo(() => new URLSearchParams(search || ""), [search]);
const seg = pathname.split("/").filter(Boolean);
/* Sammeltitel für jede Route. Ein einzelner Beitrag meldet seinen eigenen,
sobald die Daten da sind, und behält dann Vorrang. Wo hier null steht, wartet
der Titel genau darauf. */
useEffect(() => {
const named = {
wissen: seg[1] && seg[1] !== "alle" ? null : BB.t("Wissensbasis"),
forum: BB.t("Forum"),
datenbank: BB.t("Datenbank"),
uebungen: BB.t("Datenbank"),
beitrag: null,
suche: BB.t("Suchergebnisse"),
auswertung: BB.t("Split-Analyse"),
molekuelkarte: BB.t("Molekülkarte"),
neu: BB.t("Beitrag schreiben"),
bearbeiten: BB.t("Bearbeiten"),
u: null,
login: BB.t("Einloggen"),
registrieren: BB.t("Profil erstellen"),
danke: BB.t("Danke."),
"passwort-neu": BB.t("Neues Passwort setzen"),
faq: "FAQ",
impressum: "Impressum",
agb: "AGB",
einstellungen: BB.t("Einstellungen"),
verwaltung: ({
inhalte: "Inhalte · Verwaltung",
luna: "Luna-Queue · Verwaltung",
kategorien: "Kategorien · Verwaltung",
tags: "Tags · Verwaltung",
matrix: "Matrix · Verwaltung",
nutzer: "Nutzer · Verwaltung",
wartung: "Wartung · Verwaltung",
analytics: "Analytics · Verwaltung",
})[seg[1]] || BB.t("Verwaltung"),
};
const key = seg[0] || "";
const title = !key ? BB.t("Analysen, Wissen und Molekülkarte") : key in named ? named[key] : BB.t("Diese Seite gibt es nicht");
BB.setTitle(title, { fallback: true });
}, [pathname, search]);
if (pathname === "/") return ;
if (seg[0] === "wissen") {
if (seg[1] === "alle") return ;
if (seg[1]) return ;
return (params.get("cat") || params.get("q") || params.get("unread"))
?
: ;
}
if (seg[0] === "forum") {
if (!BB.FLAGS.forumEnabled) {
return ;
}
return ;
}
if (seg[0] === "datenbank") return ;
// Alte Lesezeichen bleiben erreichbar und landen direkt im passenden
// Datenbankbereich. Die öffentliche Navigation zeigt nur noch /datenbank.
if (seg[0] === "uebungen") return ;
if (seg[0] === "suche") return ;
if (seg[0] === "auswertung") return ;
if (seg[0] === "molekuelkarte") return ;
if (seg[0] === "beitrag" && seg[1]) {
if (!BB.FLAGS.forumEnabled) {
return ;
}
return ;
}
if (seg[0] === "neu") {
// Ohne offenes Forum kann nur die Redaktion in die Wissensbasis schreiben.
if (!BB.FLAGS.forumEnabled && !isEditor) {
return ;
}
return ;
}
if (seg[0] === "bearbeiten" && seg[1]) return ;
if (seg[0] === "u" && seg[1]) return ;
if (seg[0] === "login") return ;
if (seg[0] === "registrieren") {
if (!BB.FLAGS.registrationEnabled) {
return ;
}
return ;
}
if (seg[0] === "passwort-neu") return ;
if (seg[0] === "danke") return ;
if (seg[0] === "faq") return ;
if (seg[0] === "impressum") return ;
if (seg[0] === "agb") return ;
if (seg[0] === "datenschutz") return ;
if (seg[0] === "einstellungen") return ;
if (seg[0] === "verwaltung") return ;
return (
{BB.t("Zur Startseite")}}>
{BB.t("Vielleicht hilft die Suche oben weiter.")}
);
}
/* --- Fehlergrenze --------------------------------------------------------- */
/** React haengt bei einer Ausnahme im Render den gesamten Baum aus. Ohne diese
Grenze wird aus einem Fehler in einer Ansicht eine schwarze Seite, und zwar
ohne jeden Hinweis darauf, was los ist.
Der haeufigste Fall in diesem Aufbau ohne Build-Schritt: eine offene Seite
stammt von vor einer Aenderung und kennt eine neu hinzugekommene .jsx-Datei
nicht. Dann ist die Komponente schlicht nicht definiert. Das faengt der
Boundary ab und sagt genau das. */
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
console.error("Fehler in der Ansicht:", error, info && info.componentStack);
}
componentDidUpdate(prev) {
// Beim Wechsel der Route neu versuchen, sonst bleibt die Meldung kleben.
if (prev.resetKey !== this.props.resetKey && this.state.error) {
this.setState({ error: null });
}
}
render() {
const err = this.state.error;
if (!err) return this.props.children;
const meldung = String(err.message || err);
const fehlt = (meldung.match(/^(\w+) is not defined/) || [])[1];
const bausteine = eingebundeneBausteine();
// Fehlt ein Bezeichner, gibt es genau zwei Ursachen, und sie fuehren zu
// verschiedenen Schritten. Die Unterscheidung steht hier, damit sie nicht
// erst in der Browser-Konsole gesucht werden muss.
let hinweis = BB.t("Beim Aufbau ist ein Fehler aufgetreten. Die Einzelheiten stehen in der Konsole des Browsers.");
if (fehlt) {
const datei = bausteineFuer(fehlt, bausteine);
hinweis = datei
? BB.t("Die Datei ist eingebunden, wurde aber nicht ausgeführt. Das ist ein Fehler im Browser, nicht auf dem Server. Bitte die Konsole öffnen, dort steht die Ursache.")
: BB.t("Die geöffnete Seite stammt von vor der letzten Änderung. Ein Neuladen mit Strg+F5 behebt das.");
}
return (
!
{BB.t("Diese Ansicht konnte nicht geladen werden")}
);
}
}
/** Welche .jsx-Dateien stehen als Babel-Skript in der Seite? */
function eingebundeneBausteine() {
try {
return Array.from(document.querySelectorAll('script[type="text/babel"]'))
.map((s) => (s.getAttribute("src") || "inline").split("/").pop());
} catch (e) {
return [];
}
}
/** Ist die Datei eingebunden, in der der fehlende Bezeichner stehen müsste? */
function bausteineFuer(name, bausteine) {
const datei = name.replace(/Page$/, "").toLowerCase() + ".jsx";
return bausteine.find((b) => b.toLowerCase() === datei) || null;
}
/** Fades the main region on every route change, cheap continuity between views. */
function PageTransition({ children }) {
const { path } = useRouter();
const ref = useRef(null);
const key = path.split("?")[0];
useEffect(() => {
const el = ref.current;
if (!el || BB.reducedMotion() || typeof anime === "undefined") return;
anime.remove(el);
anime({ targets: el, opacity: [0, 1], translateY: [8, 0], duration: 420, easing: "easeOutQuad" });
}, [key]);
return {children};
}
/* --- root ---------------------------------------------------------------- */
function Shell() {
const { path } = useRouter();
const [categories, setCategories] = useState([]);
const [stats, setStats] = useState({ wiki: 0, community: 0, members: 0, categories: 0, comments: 0 });
const reloadCategories = useCallback(
() => BB.get("/categories").then(setCategories).catch(() => {}),
[]
);
useEffect(() => {
reloadCategories();
BB.get("/stats").then(setStats).catch(() => {});
}, [reloadCategories]);
return (
<>
>
);
}
function App() {
const [user, setUser] = useState(null);
const [ready, setReady] = useState(false);
const [locale, setLocaleState] = useState(BB.localeKey);
const [textsReady, setTextsReady] = useState(false);
const lang = (BB.LOCALES[locale] || BB.LOCALES[BB.DEFAULT_LOCALE]).lang;
useEffect(() => {
BB.get("/me")
.then(setUser)
.catch(() => setUser(null))
.finally(() => setReady(true));
}, []);
// Oberflächentexte holen, bevor gerendert wird, sonst blitzt Deutsch auf
useEffect(() => {
let alive = true;
setTextsReady(false);
BB.loadUiStrings(lang, BB.UI_STRINGS).finally(() => { if (alive) setTextsReady(true); });
return () => { alive = false; };
}, [lang]);
const setLocale = useCallback((next) => {
const key = BB.setLocale(next);
setLocaleState(key);
}, []);
return (
{/* key erzwingt einen Neuaufbau, damit alle Daten in der neuen Sprache kommen */}
{textsReady
?
:
BB
}
);
}
// ErrorBoundary wird nur hier gebraucht. Eine `class` ist wie `const`
// dateilokal, der Export macht sie trotzdem von aussen pruefbar.
Object.assign(window, { App, Header, Footer, Routes, ErrorBoundary });