Skip to content
Snippets Groups Projects
Select Git revision
  • ae20f05e0797e962f3c7eb537d72bc89e3486abd
  • main default protected
  • old_beta_site
  • smart_caching
  • 51-endpoint-course-slow-to-load
  • dork
  • dork2
  • v2.0.8 protected
  • v2.0.7 protected
  • v2.0.6 protected
  • v2.0.5 protected
  • v2.0.4 protected
  • v2.0.3 protected
  • v2.0.2 protected
  • v2.0.1 protected
  • v2.0.0 protected
  • v1.1.10 protected
  • v1.1.9 protected
  • v1.1.8 protected
  • v1.1.7 protected
  • v1.1.6 protected
  • v1.1.5 protected
  • v1.1.4 protected
  • v1.1.3 protected
  • v1.1.2 protected
  • v1.1.1 protected
  • v1.1 protected
27 results

postcss.config.js

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    DefaultLayout.tsx 28.61 KiB
    import Link from "next/link";
    import { useRouter } from "next/router";
    import { MouseEvent, startTransition, useEffect, useState } from "react";
    import { basePath } from "@/../basepath";
    import { useBackendContext } from "./BackendProvider";
    import { ApiError } from "@/api/ApiError";
    import { useUserContext } from "./UserDataProvider";
    import { useEditMode } from "./EditModeProvider";
    import { useLanguage } from "./LanguageProvider";
    import OverlayTrigger from "react-bootstrap/OverlayTrigger";
    import Popover from "react-bootstrap/Popover";
    import AnnouncementComponent from "./AnnouncementComponent";
    import { storageGetOrDefault, storageSet } from "@/misc/Storage";
    import { TooltipButton } from "@/misc/Util";
    import { showError, showErrorToast, ErrorPage } from "@/misc/ErrorHandlers";
    import { FallbackErrorBoundary } from "./FallbackErrorBoundary";
    import { ReloadBoundary } from "@/components/ReloadBoundary";
    
    import type React from "react";
    import Collapse from "react-bootstrap/Collapse";
    import Dropdown from "react-bootstrap/Dropdown";
    import { StylizedText } from "./StylizedText";
    
    import type { GetStatusResponse } from "@/api/api_v1_types";
    
    function NavBarIcon({
        children,
        iconlib,
        icon,
        activeIcon,
        url,
        className,
    }: {
        children?: React.ReactNode;
        iconlib: string;
        icon: string;
        activeIcon?: string;
        url: string;
        className?: string;
    }) {
        const router = useRouter();
        const ENDPOINT_IS_ACTIVE = router.pathname.toLowerCase() === url.toLowerCase();
    
        const effectiveIcon = ENDPOINT_IS_ACTIVE && activeIcon ? activeIcon : icon;
    
        return (
            <Link
                className={
                    "nav-link p-2 rounded d-flex align-items-center justify-content-center " +
                    (ENDPOINT_IS_ACTIVE ? "active bg-primary " : "") +
                    className
                }
                href={url}
            >
                {iconlib === "bootstrap" ? (
                    <span aria-hidden="true" className={"me-1 bi bi-" + effectiveIcon} />
                ) : (
                    <></>
                )}
                {iconlib === "fa" ? (
                    <span aria-hidden="true" className={"me-1 fa fa-" + effectiveIcon} />
                ) : (
                    <></>
                )}
                {children}
            </Link>
        );
    }
    
    function UserField({ isUnavailable }: { isUnavailable: boolean }) {
        const api = useBackendContext();
        const [loginError, setLoginError] = useState("");
        const userContext = useUserContext();
        const [showPop, setShowPop] = useState(false);
        const [forceReload, setForceReload] = useState(0);
        const [triedRelogin, setTriedRelogin] = useState(false);
        const { editMode, setEditMode } = useEditMode();
        const [isLoggingIn, setIsLoggingIn] = useState(false);
        const { language } = useLanguage();
    
        useEffect(() => {
            if (!isUnavailable && triedRelogin === false && userContext.hasUserInfo() === false) {
                api.getAuthenticationStatus()
                    .then((resp) => {
                        startTransition(() => {
                            setTriedRelogin(true);
                            if (resp.user_info) {
                                userContext.replace(resp.user_info);
                                setForceReload((f) => f + 1);
                                setShowPop(false);
                            } else {
                                userContext.replace(null);
                            }
                        });
                    })
                    .catch((e) => {
                        startTransition(() => {
                            setTriedRelogin(true);
                            userContext.replace(null);
                            console.error("Failed to get authentication status", e);
                        });
                    });
            }
        }, [api, triedRelogin, userContext, isUnavailable]);
    
        const onFsmpiLogin = (e: React.FormEvent<HTMLFormElement>) => {
            e.preventDefault();
            const form = e.currentTarget;
            const name = form.username.value;
            const password = form.password.value;
            setIsLoggingIn(true);
    
            api.authenticateFsmpi({ username: name, password })
                .then(
                    (resp) => {
                        userContext.replace(resp.user!);
                        setForceReload(forceReload + 1);
                        setShowPop(false);
                        setLoginError("");
                    },
                    (err) => {
                        if (err instanceof ApiError) {
                            setLoginError("Error: " + err.api_message);
                        } else {
                            setLoginError("Login failed " + err.message);
                        }
                        userContext.replace(null);
                    },
                )
                .then(() => {
                    setIsLoggingIn(false);
                });
        };
    
        const onLogout = () => {
            api.logout()
                .then(() => {
                    userContext.replace(null);
                    setEditMode(false);
                    setForceReload(forceReload + 1);
                })
                .catch((e) => {
                    console.error(e);
                    showError(e, "Unable to log out");
                });
        };
    
        if (isUnavailable) {
            return (
                <TooltipButton iconClassName="bi-box-arrow-in-right">
                    {language.get("ui.login.unavailable_message")}
                </TooltipButton>
            );
        }
    
        if (triedRelogin === false) {
            return <div className="spinner-border text-primary me-2" role="status" />;
        }
    
        if (userContext.hasUserInfo() === false) {
            const loginPop = (
                <Popover>
                    <Popover.Header as="h3">Login für FSMPI</Popover.Header>
                    <Popover.Body>
                        <form onSubmit={onFsmpiLogin}>
                            <input
                                placeholder="User"
                                name="username"
                                type="text"
                                className="form-control mb-2"
                            />
                            <input
                                placeholder="Password"
                                name="password"
                                type="password"
                                className="form-control mb-3"
                            />
                            {loginError !== "" ? (
                                <div className="alert alert-danger" role="alert">
                                    {loginError}
                                </div>
                            ) : (
                                <></>
                            )}
                            <div className="d-flex align-items-center">
                                <input
                                    type="submit"
                                    value="Login"
                                    className="btn btn-primary"
                                    disabled={isLoggingIn}
                                />
                                {isLoggingIn && <div className="spinner-border ms-3" />}
                            </div>
                        </form>
                    </Popover.Body>
                </Popover>
            );
    
            return (
                <OverlayTrigger trigger="click" placement="bottom" overlay={loginPop}>
                    <button className="btn" type="button" onClick={() => setShowPop(!showPop)}>
                        <span className="bi bi-box-arrow-in-right" />
                    </button>
                </OverlayTrigger>
            );
        }
    
        return (
            <>
                {userContext.canEditStuff() && (
                    <>
                        <div
                            className="form-check form-switch me-2"
                            title="Press 'e' to toggle edit mode"
                        >
                            <input
                                className="form-check-input"
                                type="checkbox"
                                role="switch"
                                id="flexSwitchCheckDefault"
                                onChange={() => {
                                    setEditMode(!editMode);
                                }}
                                checked={editMode}
                            />
                            <label className="form-check-label" htmlFor="flexSwitchCheckDefault">
                                Edit Mode
                            </label>
                        </div>
                        <div className="vr d-none d-lg-inline-block" />
                    </>
                )}
                <Dropdown className="ms-1">
                    <Dropdown.Toggle variant="" style={{ padding: "10px 6px" }}>
                        {userContext.getUserInfo()!.name} <span className="caret" />
                    </Dropdown.Toggle>
                    <Dropdown.Menu className="me-2">
                        <li>
                            <Dropdown.Item as={Link} href="/internal/dbstatus">
                                DB-Status
                            </Dropdown.Item>
                        </li>
                        <li>
                            <Dropdown.Item as={Link} href="/internal/sort/log">
                                Sortierlog
                            </Dropdown.Item>
                        </li>
                        <li>
                            <Dropdown.Item as={Link} href="/internal/changelog">
                                Changelog
                            </Dropdown.Item>
                        </li>
                        <li>
                            <Dropdown.Item as={Link} href="/internal/jobs/overview">
                                Jobs
                            </Dropdown.Item>
                        </li>
                        <li>
                            <Dropdown.Item as={Link} href="/internal/feedback">
                                Feedback
                            </Dropdown.Item>
                        </li>
                        <li className="dropdown-divider" />
                        <li>
                            <Dropdown.Item as={Link} href="/internal/user">
                                Settings
                            </Dropdown.Item>
                        </li>
                        <li className="dropdown-divider" />
                        <li>
                            <Dropdown.Item onClick={onLogout}>Logout</Dropdown.Item>
                        </li>
                    </Dropdown.Menu>
                </Dropdown>
            </>
        );
    }
    
    function Search({ className }: { className?: string }) {
        const router = useRouter();
        const [query, setQuery] = useState("");
        const { language } = useLanguage();
    
        const onSearchSubmit = (e: React.FormEvent<HTMLFormElement>) => {
            e.preventDefault();
            let form = e.currentTarget;
            let query = form.query.value;
            if (query === "") router.push("/search");
            router.push("/search?q=" + encodeURIComponent(query));
        };
        const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
            setQuery(e.target.value);
        };
    
        return (
            <form role="search" className={"d-flex " + (className ?? "")} onSubmit={onSearchSubmit}>
                <div className="input-group">
                    <input
                        className="form-control"
                        type="text"
                        name="query"
                        placeholder={language.get("ui.generic.search")}
                        value={query}
                        onChange={onChange}
                    />
                    <button className="btn btn-primary" type="submit">
                        <i className="bi bi-search" />
                    </button>
                </div>
            </form>
        );
    }
    
    function NavBar({ status }: { status?: GetStatusResponse }) {
        const [navbarOpen, setNavbarOpen] = useState(false);
        const { language, setLanguageCode } = useLanguage();
    
        useEffect(() => {
            try {
                let currentTheme = document.documentElement.getAttribute("data-bs-theme");
                if (storageGetOrDefault("theme", currentTheme) !== currentTheme) {
                    document.documentElement.classList.add("no-transition");
                    document.documentElement.setAttribute(
                        "data-bs-theme",
                        storageGetOrDefault("theme", currentTheme),
                    );
                    document.documentElement.classList.remove("no-transition");
                }
            } catch (e) {
                console.error("Could not load theme from local storage", e);
            }
        }, []);
    
        const switchTheme = () => {
            const theme = document.documentElement.getAttribute("data-bs-theme");
            if (theme === "dark") {
                document.documentElement.setAttribute("data-bs-theme", "light");
            } else {
                document.documentElement.setAttribute("data-bs-theme", "dark");
            }
            try {
                storageSet("theme", theme === "dark" ? "light" : "dark");
            } catch (e) {
                console.error("Could not save theme to local storage", e);
            }
        };
    
        const switchLanguage = () => {
            setLanguageCode(language.getCode() === "de" ? "en" : "de");
        };
    
        const toggleNavbar = () => {
            setNavbarOpen((o) => !o);
        };
    
        const switchToOldSite = (event: MouseEvent) => {
            if (window.location.pathname.indexOf(basePath) !== -1) {
                event.preventDefault();
                let newPath = window.location.pathname;
                if (!newPath.startsWith("/")) newPath = "/" + newPath;
                window.location.href = "https://video.fsmpi.rwth-aachen.de" + newPath;
            }
        };
    
        return (
            <nav className={"hidden-print navbar navbar-expand-lg mb-3 bg-body-tertiary"}>
                <div className="container-fluid">
                    <div className="d-flex d-lg-none flex-grow-1">
                        <Link className="navbar-brand" href="/" style={{ padding: "3px" }}>
                            <img
                                alt="VideoAG"
                                src={`${basePath}/static/logo.png`}
                                width={44}
                                height={44}
                            />
                        </Link>
                        <NavBarIcon
                            iconlib="bootstrap"
                            icon="house-door-fill"
                            url="/"
                            className="flex-grow-1 text-center"
                        />
                        <NavBarIcon
                            iconlib="bootstrap"
                            icon="film"
                            url="/courses"
                            className="flex-grow-1 text-center"
                        />
                        <NavBarIcon
                            iconlib="bootstrap"
                            icon="question-circle-fill"
                            activeIcon="question-circle"
                            url="/faq"
                            className="flex-grow-1 text-center"
                        />
                        <NavBarIcon
                            iconlib="bootstrap"
                            icon="envelope-fill"
                            activeIcon="envelope"
                            url="/feedback"
                            className="flex-grow-1 text-center"
                        />
                        <a
                            className={
                                "nav-link p-2 rounded flex-grow-1 text-center d-flex align-items-center justify-content-center"
                            }
                            href={"/"}
                            onClick={switchToOldSite}
                            style={{ color: "#c66" }}
                        >
                            <span
                                aria-hidden="true"
                                className={"me-1 bi bi-arrow-down-left-square"}
                            ></span>
                        </a>
                    </div>
    
                    <button
                        className="navbar-toggler ms-2"
                        type="button"
                        onClick={toggleNavbar}
                        aria-label="Toggle navigation"
                    >
                        <span className="navbar-toggler-icon" />
                    </button>
    
                    <Collapse in={navbarOpen} className={"navbar-collapse"}>
                        <div className="">
                            <div className="d-flex align-items-center w-100 flex-column flex-sm-row mt-3 mt-lg-0">
                                <ul className="navbar-nav align-items-center d-none d-lg-flex">
                                    <Link
                                        className="navbar-brand d-none d-lg-block"
                                        href="/"
                                        style={{ padding: "3px" }}
                                    >
                                        <img
                                            alt="VideoAG"
                                            src={`${basePath}/static/logo.png`}
                                            width={44}
                                            height={44}
                                        />
                                    </Link>
                                    <li className="nav-item">
                                        <NavBarIcon
                                            iconlib="bootstrap"
                                            icon="house-door-fill"
                                            activeIcon="house-door"
                                            url="/"
                                            className="mx-1"
                                        >
                                            Home
                                        </NavBarIcon>
                                    </li>
                                    <li className="nav-item">
                                        <NavBarIcon
                                            iconlib="bootstrap"
                                            icon="film"
                                            url="/courses"
                                            className="mx-1"
                                        >
                                            Videos
                                        </NavBarIcon>
                                    </li>
                                    <li className="nav-item">
                                        <NavBarIcon
                                            iconlib="bootstrap"
                                            icon="question-circle-fill"
                                            activeIcon="question-circle"
                                            url="/faq"
                                            className="mx-1"
                                        >
                                            FAQ
                                        </NavBarIcon>
                                    </li>
                                    <li className="nav-item">
                                        <NavBarIcon
                                            iconlib="bootstrap"
                                            icon="envelope-fill"
                                            activeIcon="envelope"
                                            url="/feedback"
                                            className="mx-1"
                                        >
                                            Feedback
                                        </NavBarIcon>
                                    </li>
                                    <a
                                        className={"nav-link p-2 rounded mx-1"}
                                        href={"https://video.fsmpi.rwth-aachen.de"}
                                        onClick={switchToOldSite}
                                        style={{ color: "#c66" }}
                                    >
                                        <span
                                            aria-hidden="true"
                                            className={"me-1 bi bi-arrow-down-left-square"}
                                        ></span>
                                        Zur alten Seite
                                    </a>
                                </ul>
    
                                <div className="flex-fill d-none d-lg-block" />
    
                                <Search className="me-1 flex-sm-fill w-sm-auto" />
    
                                <div className="d-flex align-items-center">
                                    <UserField isUnavailable={status?.status === "unavailable"} />
    
                                    <div>
                                        <button className={"btn"} type="button" onClick={switchTheme}>
                                            <span aria-hidden="true" className={"bi bi-moon-stars"} />
                                        </button>
                                    </div>
                                    <div>
                                        <button
                                            className={"btn"}
                                            type="button"
                                            onClick={switchLanguage}
                                        >
                                            <span aria-hidden="true" className={"bi bi-globe"} />
                                            <small className="text-muted ms-1">
                                                {language.getCode()}
                                            </small>
                                        </button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </Collapse>
                </div>
            </nav>
        );
    }
    
    function Footer({ status }: { status?: GetStatusResponse }) {
        const { language } = useLanguage();
        const userContext = useUserContext();
        const hasUserInfo = userContext.hasUserInfo();
    
        return (
            <footer className={"footer bg-body-tertiary"}>
                <div className="px-2 d-flex align-items-center">
                    <ul className="list-inline py-2 mb-0">
                        <li className="list-inline-item">
                            <Link href="/imprint">{language.get("ui.footer.imprint")}</Link>
                        </li>
                        <li className="list-inline-item">
                            <a href="http://www.vampir.rwth-aachen.de/">Vampir e.V.</a>
                        </li>
                        <li className="list-inline-item">
                            <a href="https://www.youtube.com/channel/UCxh5snRN7yZyBsytNbGNuEQ">
                                YouTube
                            </a>
                        </li>
                        <li className="list-inline-item">
                            <a href="https://www.facebook.com/videoag">Facebook</a>
                        </li>
                        <li className="list-inline-item">
                            <a href="https://twitter.com/rwthvideo">
                                {language.get("ui.footer.twitter")}
                            </a>
                        </li>
                        <li className="list-inline-item">
                            <Link href="/licenses">Licenses</Link>
                        </li>
                    </ul>
                    <div className="flex-fill" />
                    {hasUserInfo ? (
                        <>
                            <div className="d-block">
                                Front:{" "}
                                <a
                                    href={
                                        "https://git.fsmpi.rwth-aachen.de/videoag/frontend/-/tree/" +
                                        (process.env.NEXT_PUBLIC_GIT_COMMIT_HASH ?? "live_production")
                                    }
                                >
                                    {process.env.NEXT_PUBLIC_GIT_COMMIT_HASH ?? "unknown"}
                                </a>
                                ; Back:{" "}
                                <a
                                    href={
                                        "https://git.fsmpi.rwth-aachen.de/videoag/backend_api/-/tree/" +
                                        (status?.git_commit_hash ?? "live_production")
                                    }
                                >
                                    {status?.git_commit_hash ?? "unknown"}
                                </a>
                                , {status?.status ?? "unknown"}
                            </div>
                        </>
                    ) : (
                        <>
                            <div className="d-none d-md-block">
                                <StylizedText markdown mapParagraphToSpan>
                                    {language.get("ui.footer.advertisement")}
                                </StylizedText>
                            </div>
                            <div className="d-md-none">
                                <StylizedText markdown mapParagraphToSpan>
                                    {language.get("ui.footer.advertisement_short")}
                                </StylizedText>
                            </div>
                        </>
                    )}
                </div>
            </footer>
        );
    }
    
    export default function DefaultLayout({ children }: { children: React.ReactNode }) {
        const route = useRouter().pathname;
        const api = useBackendContext();
        const userContext = useUserContext();
        const hasUserInfo = userContext.hasUserInfo();
        const { setEditMode } = useEditMode();
        const { language } = useLanguage();
        const [status, setStatus] = useState<GetStatusResponse>();
        const [forceReload, setForceReload] = useState(0);
    
        const isEmbed = route === "/dynamic_embed";
    
        useEffect(() => {
            if (isEmbed) return;
            const onKeydown = (e: KeyboardEvent) => {
                if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
                    return;
                if (e.key === "e" && userContext.canEditStuff()) {
                    setEditMode((e) => !e);
                }
            };
    
            document.addEventListener("keydown", onKeydown);
            return () => {
                document.removeEventListener("keydown", onKeydown);
            };
        }, [isEmbed, setEditMode, userContext]);
    
        useEffect(() => {
            api.getStatus()
                .then(setStatus)
                .catch((error) => {
                    //TODO
                    console.error("Error while fetching status", error);
                });
        }, [api, hasUserInfo, forceReload]);
    
        const isUnavailable = status?.status === "unavailable";
    
        if (isEmbed) {
            //TODO announcements
            if (isUnavailable) {
                return (
                    <div
                        className={"hidden-print alert d-flex align-items-center alert-info"}
                        role="alert"
                    >
                        {language.get("site.unavailable_message")}
                    </div>
                );
            }
            return <div style={{ width: "100vw", height: "100vh" }}>{children}</div>;
        }
    
        return (
            <div className="d-flex flex-column" style={{ minHeight: "100vh" }}>
                <ReloadBoundary reloadFunc={() => setForceReload((v) => v + 1)}>
                    <NavBar status={status} />
                    <div className="container-fluid mb-2 flex-grow-1">
                        <div className="row">
                            <div className="col-12 offset-lg-1 col-lg-10">
                                <AnnouncementComponent
                                    announcements={status?.announcements ?? []}
                                    forceShowHidden={isUnavailable}
                                />
                                {status?.status === "unavailable" ? (
                                    <div
                                        className={
                                            "hidden-print alert d-flex align-items-center alert-info"
                                        }
                                        role="alert"
                                    >
                                        {language.get("site.unavailable_message")}
                                    </div>
                                ) : (
                                    <ReloadBoundary
                                        reloadFunc={() => {
                                            if (process.env.NODE_ENV === "development") {
                                                showErrorToast(
                                                    "Reload boundary without reload! (Application is now in inconsistent state, you should reload)",
                                                );
                                            } else {
                                                window.location.reload();
                                            }
                                        }}
                                    >
                                        <FallbackErrorBoundary
                                            fallback={(e: any) => (
                                                <ErrorPage error={e} objectName="Seite" />
                                            )}
                                        >
                                            {children}
                                        </FallbackErrorBoundary>
                                    </ReloadBoundary>
                                )}
                            </div>
                        </div>
                    </div>
                    <Footer status={status} />
    
                    {
                        status?.is_debug === true && (
                            <div className="watermark">debug mode</div>
                        ) /* You can disable debug mode in the configuration file of the backend api */
                    }
                </ReloadBoundary>
            </div>
        );
    }