Skip to content
Snippets Groups Projects
Select Git revision
  • 0c1c9c49865d0bd7a348ea64a0fa2dc6908afb89
  • master default protected
  • postgres_integration
  • s3compatible
  • intros
  • bootstrap4
  • modules
7 results

edit.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    index.tsx 13.38 KiB
    import React from "react";
    import { Suspense, useEffect, useState } from "react";
    import { useSearchParams } from "next/navigation";
    import { useRouter } from "next/router";
    
    import type { GetHomepageResponse, featured, int } from "@/videoag/api/types";
    import { useApi } from "@/videoag/api/ApiProvider";
    import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
    import { LectureCard } from "@/videoag/course/Lecture";
    import { LectureLiveLabel } from "@/videoag/course/LiveLabel";
    import { ErrorComponent, showError, showWarningToast } from "@/videoag/error/ErrorDisplay";
    import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary";
    import { useLanguage } from "@/videoag/localization/LanguageProvider";
    import { datetimeToStringOnlyDate, datetimeToStringOnlyTime } from "@/videoag/miscellaneous/Formatting";
    import { ReloadBoundary, useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
    import {
        ResourceType,
        fetchDataWrapper,
        useDebounceWithArgument,
    } from "@/videoag/miscellaneous/PromiseHelpers";
    import { useEditMode } from "@/videoag/object_management/EditModeProvider";
    import {
        OMCreate,
        OMDelete,
        OMEdit,
        OMHistory,
        EmbeddedOMFieldComponent,
    } from "@/videoag/object_management/OMConfigComponent";
    import { Search } from "@/videoag/site/DefaultLayout";
    import { basePath } from "@/../basepath";
    
    import { SearchResults } from "./search";
    
    function FeatureCard({ data, all_featured }: { data: featured; all_featured: featured[] }) {
        const { editMode } = useEditMode();
        const { language } = useLanguage();
        const reloadFunc = useReloadBoundary();
        const api = useApi();
    
        const isFirst = all_featured[0].id === data.id;
        const isLast = all_featured[all_featured.length - 1].id === data.id;
    
        const movePanel = (dir: int) => {
            const myIndex = all_featured.findIndex((f) => f.id === data.id);
            api.updateOMObject("featured", data.id!, {
                updates: {
                    display_priority: dir == -1 ? myIndex - 2 : myIndex + 1,
                },
                expected_current_values: {
                    display_priority: myIndex,
                },
            })
                .then(reloadFunc)
                .catch((error) => {
                    showError(error, "Error while moving panel");
                });
        };
    
        let bgColor = "";
        if (data.visible === false) {
            bgColor = "bg-danger-subtle";
        }
    
        return (
            <div className={"card mb-3 " + bgColor}>
                <div className="card-body">
                    <h5 className="card-title d-flex align-items-center">
                        <div className="overflow-hidden pb-1">
                            <EmbeddedOMFieldComponent
                                object_type="featured"
                                object_id={data.id!}
                                field_id="title"
                                field_type="string"
                                initialValue={data.title}
                                allowMarkdown={true}
                            />
                        </div>
                        {editMode && (
                            <EmbeddedOMFieldComponent
                                object_type="featured"
                                object_id={data.id!}
                                field_id="visible"
                                field_type="boolean"
                                initialValue={data.visible}
                            />
                        )}
                        <div className="flex-fill" />
    
                        {editMode && (
                            <div>
                                <OMHistory object_type="featured" object_id={data.id!} />
    
                                <div className="btn-group m-1" role="group">
                                    <button
                                        type="button"
                                        className="btn btn-outline-info"
                                        disabled={isFirst}
                                        onClick={() => movePanel(-1)}
                                    >
                                        <i className="bi bi-arrow-up" />
                                    </button>
                                    <button
                                        type="button"
                                        className="btn btn-outline-info"
                                        disabled={isLast}
                                        onClick={() => movePanel(1)}
                                    >
                                        <i className="bi bi-arrow-down" />
                                    </button>
                                </div>
    
                                <OMEdit object_type="featured" object_id={data.id!} />
                                <OMDelete object_type="featured" object_id={data.id!} />
                            </div>
                        )}
                    </h5>
    
                    {data.type === "image" && data.image_url && (
                        <img
                            src={data.image_url}
                            alt={data.title}
                            className="img-fluid rounded"
                            style={{ maxHeight: "500px" }}
                        />
                    )}
    
                    {/* Add support for type lecture and course */}
    
                    <EmbeddedOMFieldComponent
                        object_type="featured"
                        object_id={data.id!}
                        field_id="text"
                        field_type="string"
                        initialValue={data.text}
                        className="flex-fill"
                        allowMarkdown={true}
                    />
    
                    {data.visible === false && (
                        <>
                            <hr />
                            <small className="text-muted">
                                {language.get("ui.generic.object_invisible")}
                            </small>
                        </>
                    )}
                </div>
            </div>
        );
    }
    
    function FeaturedContent({ homepageData }: { homepageData: ResourceType<GetHomepageResponse> }) {
        const data = homepageData.read()!;
    
        return data.featured.map((f) => (
            <FeatureCard key={f.title} data={f} all_featured={data.featured} />
        ));
    }
    
    function UpcomingUploads({ homepageData }: { homepageData: ResourceType<GetHomepageResponse> }) {
        const data = homepageData.read()!;
        const { language } = useLanguage();
    
        if (data.upcoming_lectures.length === 0) {
            return (
                <span className="disable-last-paragraph-spacing">
                    {language.getStyled("ui.index.no_scheduled_recordings", true)}
                </span>
            );
        }
    
        let dates = []; // group by day/date
        for (let lecture of data.upcoming_lectures) {
            let date_string = datetimeToStringOnlyDate(lecture.time);
    
            let date_index = dates.findIndex((date) => date.date === date_string);
            if (date_index === -1) {
                dates.push({ date: date_string, lectures: [lecture] });
            } else {
                dates[date_index].lectures.push(lecture);
            }
        }
    
        return (
            <ul className="list-group">
                {dates.map((date, index) => (
                    <li className="list-group-item" key={index}>
                        <strong>{date.date}</strong>
                        <ul className="list-group">
                            {date.lectures.map((lecture, index) => {
                                let course_data = data.course_context[lecture.course_id];
                                return (
                                    <li
                                        className="list-group-item list-group-item-condensed"
                                        style={{ border: "none" }}
                                        key={index}
                                    >
                                        {datetimeToStringOnlyTime(lecture.time)}{" "}
                                        <a href={`/${course_data.handle}`}>{course_data.full_name}</a>
                                        {": "}
                                        <a href={`/${course_data.handle}#lecture-${lecture.id}`}>
                                            {lecture.title}
                                        </a>
                                        {lecture.location.length > 0 && ` (${lecture.location})`}{" "}
                                        <LectureLiveLabel lecture={lecture} />
                                    </li>
                                );
                            })}
                        </ul>
                    </li>
                ))}
            </ul>
        );
    }
    
    function RecentUploads({ homepageData }: { homepageData: ResourceType<GetHomepageResponse> }) {
        const data = homepageData.read()!;
    
        return (
            <ul className="list-group videopreview">
                {data.latest_lectures.map((lecture, index) => {
                    let course_data = data.course_context[lecture.course_id];
                    return (
                        <li className="list-group-item" key={index}>
                            <LectureCard lecture={lecture} course={course_data} />{" "}
                        </li>
                    );
                })}
            </ul>
        );
    }
    
    function ModButtons() {
        const { language } = useLanguage();
    
        return (
            <div className="d-flex mb-2">
                <div className="flex-fill" />
                <OMCreate object_type="featured">
                    <button className="btn btn-secondary" type="button">
                        <i className="bi bi-plus" />
                        {language.get("ui.index.create_panel")}
                    </button>
                </OMCreate>
            </div>
        );
    }
    
    export default function Home() {
        const api = useApi();
        const authStatus = useAuthStatus();
    
        const [homepageData, setHomepageData] = useState<ResourceType<GetHomepageResponse>>();
        const [query, setQuery] = useState("");
        const [updateQueryDeferred, _] = useDebounceWithArgument(setQuery, 500);
    
        const hasUserInfo = authStatus.hasUserInfo();
        const { editMode } = useEditMode();
        const { language } = useLanguage();
    
        // legacy redirects
        const searchParams = useSearchParams();
        const router = useRouter();
        const handleLegacyRedirects = () => {
            if (searchParams.has("course")) {
                router.replace(`/${searchParams.get("course")}`);
                return;
            }
            if (searchParams.has("view")) {
                if (searchParams.get("view") === "player") {
                    showWarningToast(
                        "Die URL-Struktur hat sich geändert. Bitte finden sie ihr gesuchtes Video über die Suchfunktion.",
                    );
                }
                if (searchParams.get("view") === "faq") {
                    router.replace("/faq");
                    return;
                }
            }
        };
        useEffect(handleLegacyRedirects, [searchParams, router]);
    
        const reloadData = () => {
            setHomepageData(fetchDataWrapper(api.getHomepage()));
        };
    
        useEffect(reloadData, [api, hasUserInfo]);
    
        return (
            <ReloadBoundary reloadFunc={reloadData}>
                <FallbackErrorBoundary
                    fallback={(e: any) => <ErrorComponent error={e} objectName="Seite" />}
                >
                    <div className="row justify-content-center w-100 mt-3 mb-4 mx-0">
                        <div className="col-md-5 px-0 text-center">
                            <div className="d-flex justify-content-center align-items-center">
                                <img
                                    alt="VideoAG"
                                    src={`${basePath}/static/logo.png`}
                                    width={88}
                                    height={88}
                                />
                                <h1 className="ms-3">VideoAG</h1>
                            </div>
    
                            <Search className="mt-3" queryCallback={updateQueryDeferred} />
                        </div>
                    </div>
                    <hr />
                    {query.length > 0 && (
                        <>
                            <SearchResults query={query} />
                            <hr />
                        </>
                    )}
                    {homepageData && (
                        <div className="row">
                            {editMode && <ModButtons />}
                            <Suspense fallback={<div className="spinner-border" />}>
                                <div className="col-md-6">
                                    <FeaturedContent homepageData={homepageData} />
                                </div>
                                <div className="col-md-6">
                                    <div className="card mb-3">
                                        <div className="card-body">
                                            <h5 className="card-title">
                                                {language.get("ui.index.next_recordings")}
                                            </h5>
    
                                            <UpcomingUploads homepageData={homepageData} />
                                        </div>
                                    </div>
                                    <div className="card mb-3">
                                        <div className="card-body">
                                            <h5 className="card-title">
                                                {language.get("ui.index.recent_videos")}
                                            </h5>
    
                                            <RecentUploads homepageData={homepageData} />
                                        </div>
                                    </div>
                                </div>
                            </Suspense>
                        </div>
                    )}
                </FallbackErrorBoundary>
            </ReloadBoundary>
        );
    }