Select Git revision
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>
);
}