diff --git a/videoag_new_frontend/src/components/FallbackErrorBoundary.tsx b/videoag_new_frontend/src/components/FallbackErrorBoundary.tsx index f2231555166ecfdb48272b398783396c3a45417a..7ddfe4bcb95bd0d02bf5f89710d67693ee3d1a97 100644 --- a/videoag_new_frontend/src/components/FallbackErrorBoundary.tsx +++ b/videoag_new_frontend/src/components/FallbackErrorBoundary.tsx @@ -13,7 +13,7 @@ export class FallbackErrorBoundary extends React.Component<any, { error?: any }> } componentDidCatch(error: any, info: ErrorInfo) { - console.error(error, info); + console.error("The following error was caught by FallbackErrorBoundary:", error, info); } clearError() { diff --git a/videoag_new_frontend/src/components/Player.tsx b/videoag_new_frontend/src/components/Player.tsx index 359228b3ad7caf2062cb15ae4cbac6b0a235b1ee..ce3b781815ff9f936a2e409a24d3ed5b46131c0b 100644 --- a/videoag_new_frontend/src/components/Player.tsx +++ b/videoag_new_frontend/src/components/Player.tsx @@ -1,8 +1,6 @@ import { MouseEvent, useEffect, useRef, useState } from "react"; import "video.js/dist/video-js.min.css"; import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css"; -import { Backend } from "@/api/Backend"; -import { useLoaderData, useNavigate, useParams } from "react-router-dom"; import Link from "next/link"; import { useBackendContext } from "./BackendProvider"; import { timestampToString, stringToStyledHtml } from "@/misc/Formatting"; @@ -19,7 +17,6 @@ import { showError } from "@/misc/ErrorHandlers"; import type React from "react"; import { - AuthenticationStatusResponse, GetCourseResponse, authentication_method, authentication_methods, @@ -28,33 +25,8 @@ import { lecture, } from "@/api/api_v1_types"; import Dropdown from "react-bootstrap/Dropdown"; - -export function getLectureDataLoader(api: Backend, hasUserInfo: boolean) { - const actualLoader = async ({ - params, - }: { - params: { course_id_string?: string; lecture_id?: string }; - }) => { - let course: GetCourseResponse = await api.getCourse(params.course_id_string!, true); - let lecture = course.lectures!.find((l) => l.id === parseInt(params.lecture_id!)); - if (lecture === undefined) { - throw new Error("Lecture not found"); - } - - let perms: AuthenticationStatusResponse = { - authenticated_methods: ["public"], - is_lecture_authenticated: true, - }; - if ( - lecture.authentication_methods.includes("public") === false && - lecture.authentication_methods.length > 0 - ) { - perms = await api.getAuthenticationStatus({ lecture_id: lecture.id }); - } - return { course, perms, loaderHadUserInfo: hasUserInfo }; - }; - return actualLoader; -} +import { ResourceType } from "@/misc/PromiseHelpers"; +import { PlayerData } from "@/pages/dynamic_player"; function VideoPlayer({ lecture, className }: { lecture: lecture; className?: string }) { const videoRef = useRef<HTMLVideoElement>(null); @@ -185,10 +157,17 @@ function VideoPlayer({ lecture, className }: { lecture: lecture; className?: str ); } -function KapitelPopover({ timeStr, closePop }: { timeStr: string; closePop: () => void }) { +function KapitelPopover({ + timeStr, + closePop, + lectureId, +}: { + timeStr: string; + closePop: () => void; + lectureId: number; +}) { const userContext = useUserContext(); const api = useBackendContext(); - const lecture_id = parseInt(useParams().lecture_id!); const reloadFunc = useReloadBoundary(); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { @@ -207,7 +186,7 @@ function KapitelPopover({ timeStr, closePop }: { timeStr: string; closePop: () = if (userContext.canEditStuff()) { api.createOMObject("chapter", { parent_type: "lecture", - parent_id: lecture_id, + parent_id: lectureId, values: { start_time: time, name: text, @@ -222,7 +201,7 @@ function KapitelPopover({ timeStr, closePop }: { timeStr: string; closePop: () = showError(e, "Unable to create chapter"); }); } else { - api.suggestChapter(lecture_id, { + api.suggestChapter(lectureId, { start_time: time, name: text, }) @@ -277,7 +256,7 @@ function AuthorizeHelper({ authed_methods: authentication_methods; }) { const api = useBackendContext(); - const navigate = useNavigate(); + const reloadFunc = useReloadBoundary(); const [userPwErrorState, setUserPwErrorState] = useState(); const userContext = useUserContext(); const hasUserInfo = userContext.hasUserInfo(); @@ -285,9 +264,10 @@ function AuthorizeHelper({ useEffect(() => { if (hasUserInfo === true) { // reload page when the user logs in - navigate(".", { replace: true }); + reloadFunc(); } - }, [navigate, hasUserInfo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasUserInfo]); const startOauth = (method: authentication_method) => { if (method !== "moodle" && method !== "rwth") return; @@ -299,7 +279,7 @@ function AuthorizeHelper({ let popInterval = setInterval(() => { if (pop.closed) { clearInterval(popInterval); - navigate(".", { replace: true }); + reloadFunc(); } }, 100); } @@ -315,9 +295,7 @@ function AuthorizeHelper({ let user = form.username.value; let pass = form.password.value; api.authenticatePassword({ username: user, password: pass, lecture_id: lecture.id }) - .then((res) => { - navigate(".", { replace: true }); - }) + .then(reloadFunc) .catch((e) => { setUserPwErrorState(e); }); @@ -573,14 +551,10 @@ export function DownloadSources({ ); } -export function EmbeddedPlayer() { - const { course, perms } = useLoaderData() as { - course: GetCourseResponse; - perms: AuthenticationStatusResponse; - }; +export function EmbeddedPlayer({ playerData }: { playerData: ResourceType<PlayerData> }) { + const { course, perms, lectureId } = playerData.read()!; - let lecture_id = parseInt(useParams().lecture_id!); - const lecture = course.lectures!.find((l) => l.id === lecture_id)!; + const lecture = course.lectures!.find((l) => l.id === lectureId)!; useEffect(() => { import("video.js"); @@ -608,17 +582,12 @@ export function EmbeddedPlayer() { return <>{pageContent} </>; } -export default function Player() { +export default function Player({ playerData }: { playerData: ResourceType<PlayerData> }) { + const { course, lectureId, perms, loaderHadUserInfo } = playerData.read()!; const userContext = useUserContext(); const hasUserInfo = userContext.hasUserInfo(); - const { course, perms, loaderHadUserInfo } = useLoaderData() as { - course: GetCourseResponse; - perms: AuthenticationStatusResponse; - loaderHadUserInfo: boolean; - }; - let lecture_id = parseInt(useParams().lecture_id!); - const lecture = course.lectures!.find((l) => l.id === lecture_id)!; + const lecture = course.lectures!.find((l) => l.id === lectureId)!; useEffect(() => { import("video.js"); @@ -645,7 +614,12 @@ export default function Player() { let tstr = timestampToString(timestamp); setPopContent( - <KapitelPopover timeStr={tstr} key={tstr} closePop={() => setShowPop(false)} />, + <KapitelPopover + timeStr={tstr} + key={tstr} + closePop={() => setShowPop(false)} + lectureId={lectureId} + />, ); setShowPop(true); }); diff --git a/videoag_new_frontend/src/misc/ErrorHandlers.tsx b/videoag_new_frontend/src/misc/ErrorHandlers.tsx index e507e3f098569293d3dc7328a50e10c7d9a86da6..1b1c3532e4aab8cbcdbe58144e2ed6895963e2bd 100644 --- a/videoag_new_frontend/src/misc/ErrorHandlers.tsx +++ b/videoag_new_frontend/src/misc/ErrorHandlers.tsx @@ -117,7 +117,7 @@ export function ErrorPage({ } return ( <div className="alert alert-danger" role="alert"> - <h4 className="alert-heading">Ein unerwarteter Fehler hat dich besucht</h4> + <h4 className="alert-heading">Unerwarteter Fehler!</h4> <p>{mainMessage}</p> <div> Falls das Problem länger bestehen sollte, schreib uns bitte eine Mail an{" "} @@ -131,12 +131,13 @@ export function ErrorPage({ : "Seite auf welcher der Fehler aufgetreten ist"} </li> <li>Fehler: {error.message}</li> - <li>evt. was du getan hast als der Fehler aufgetreten ist</li> + <li>Was hast du getan bevor der Fehler aufgetreten ist?</li> + <li>Wie kann man den Fehler reproduzieren?</li> </ul> Wir werden uns dann schnellst möglich darum kümmern. </div> - <button type="button" className="btn btn-success mb-2" onClick={reloadFunc}> - <span className="bi bi-arrow-counterclockwise" /> + <button type="button" className="btn btn-success mt-2" onClick={reloadFunc}> + <span className="bi bi-arrow-counterclockwise me-1" /> Neuladen </button> </div> diff --git a/videoag_new_frontend/src/pages/dynamic_embed.tsx b/videoag_new_frontend/src/pages/dynamic_embed.tsx index 6129a954bd2a83f956e3c720eee710f12af6a000..9f28b15eb79c09a765a1ac5f8ca744a05d6fae1d 100644 --- a/videoag_new_frontend/src/pages/dynamic_embed.tsx +++ b/videoag_new_frontend/src/pages/dynamic_embed.tsx @@ -1,64 +1,8 @@ import { useEffect, useState } from "react"; -import { RouterProvider, createBrowserRouter, useRouteError } from "react-router-dom"; -import nextConfig from "@/../basepath"; -import Four04 from "./404"; -import { EmbeddedPlayer, getLectureDataLoader } from "@/components/Player"; -import { useBackendContext } from "@/components/BackendProvider"; -import { ReloadBoundary } from "@/components/ReloadBoundary"; -import { ErrorPage } from "@/misc/ErrorHandlers"; -import { useUserContext } from "@/components/UserDataProvider"; - -function Custom404() { - const error = useRouteError(); - return ( - <ErrorPage - error={error} - objectName="Vorlesung" - expectedErrorCodes={[ - "unauthorized", - "access_forbidden", - "unknown_object", - "rate_limited", - ]} - /> - ); -} - -function ActualRedirector() { - const api = useBackendContext(); - const userContext = useUserContext(); - - const router = createBrowserRouter( - [ - { - path: ":course_id_string/:lecture_id/embed", - loader: getLectureDataLoader(api, userContext.hasUserInfo()), - errorElement: <Custom404 />, - element: <EmbeddedPlayer />, - }, - { - path: "404", - element: <Four04 />, - }, - { - path: "*", - element: <>* matched dynamic_embed</>, - }, - ], - { basename: nextConfig.basePath }, - ); - - const reload = () => { - router.navigate(".", { replace: true }); - }; - - return ( - <ReloadBoundary reloadFunc={reload}> - <RouterProvider router={router} /> - </ReloadBoundary> - ); -} +import { ActualRedirector } from "./dynamic_player"; +// This only exists, so that we can effectively differentiate between player and embed in DefaultLayout.tsx (to remove the page header/footer for embeds) +// Necessary so that we can render the correct layout while building the page (since we can't use the actual pathname at build time) export default function DynamicEmbed() { // The following is a hack to force nextjs to not pre-render this page at build time (which obv wouldnt make sense) const [clientsideForcer, setClientsideForcer] = useState(false); diff --git a/videoag_new_frontend/src/pages/dynamic_player.tsx b/videoag_new_frontend/src/pages/dynamic_player.tsx index d78ab9615a5a0d038fb4262b0823a0377dc0fbbd..e64327472dce6e5195bd497f5b2af2bf07fcaaec 100644 --- a/videoag_new_frontend/src/pages/dynamic_player.tsx +++ b/videoag_new_frontend/src/pages/dynamic_player.tsx @@ -1,60 +1,99 @@ -import { useEffect, useState } from "react"; -import { RouterProvider, createBrowserRouter, useRouteError } from "react-router-dom"; -import nextConfig from "@/../basepath"; +import { Suspense, useEffect, useState } from "react"; import Four04 from "./404"; -import Player, { getLectureDataLoader } from "@/components/Player"; + import { useBackendContext } from "@/components/BackendProvider"; import { ReloadBoundary } from "@/components/ReloadBoundary"; -import { ErrorPage } from "@/misc/ErrorHandlers"; import { useUserContext } from "@/components/UserDataProvider"; +import { usePathname } from "next/navigation"; +import { Backend } from "@/api/Backend"; +import { AuthenticationStatusResponse, GetCourseResponse } from "@/api/api_v1_types"; +import { ResourceType, fetchDataWrapper } from "@/misc/PromiseHelpers"; +import Player, { EmbeddedPlayer } from "@/components/Player"; +import { ErrorPage } from "@/misc/ErrorHandlers"; +import { FallbackErrorBoundary } from "@/components/FallbackErrorBoundary"; -function Custom404() { - const error = useRouteError(); - return ( - <ErrorPage - error={error} - objectName="Vorlesung" - expectedErrorCodes={[ - "unauthorized", - "access_forbidden", - "unknown_object", - "rate_limited", - ]} - /> - ); +export interface PlayerData { + course: GetCourseResponse; + perms: AuthenticationStatusResponse; + loaderHadUserInfo: boolean; + lectureId: number; } -function ActualRedirector() { +async function dataLoader( + api: Backend, + course_id_string: string, + lecture_id: string, + hasUserInfo: boolean, +): Promise<PlayerData> { + let course: GetCourseResponse = await api.getCourse(course_id_string, true); + let lecture = course.lectures!.find((l) => l.id === parseInt(lecture_id)); + if (lecture === undefined) { + throw new Error("Lecture not found"); + } + + let perms: AuthenticationStatusResponse = { + authenticated_methods: ["public"], + is_lecture_authenticated: true, + }; + if ( + lecture.authentication_methods.includes("public") === false && + lecture.authentication_methods.length > 0 + ) { + perms = await api.getAuthenticationStatus({ lecture_id: lecture.id }); + } + return { course, perms, loaderHadUserInfo: hasUserInfo, lectureId: parseInt(lecture_id) }; +} + +// This redirector handles both the regular player and the embedded player +export function ActualRedirector() { const api = useBackendContext(); const userContext = useUserContext(); + const hasUserInfo = userContext.hasUserInfo(); + const pathname = usePathname(); + const matchedParamsPlayer = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)$/); + const matchedParamsEmbed = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)\/embed$/); + const [playerData, setPlayerData] = useState<ResourceType<PlayerData>>(); - const router = createBrowserRouter( - [ - { - path: ":course_id_string/:lecture_id", - loader: getLectureDataLoader(api, userContext.hasUserInfo()), - errorElement: <Custom404 />, - element: <Player />, - }, - { - path: "404", - element: <Four04 />, - }, - { - path: "*", - element: <>* matched dynamic_player</>, - }, - ], - { basename: nextConfig.basePath }, - ); - - const reload = () => { - router.navigate(".", { replace: true }); + const reloadData = () => { + if (!matchedParamsPlayer && !matchedParamsEmbed) return; + const matched = (matchedParamsPlayer ?? matchedParamsEmbed)!; + const courseId = matched[1]; + const lectureId = matched[2]; + setPlayerData( + fetchDataWrapper(dataLoader(api, courseId, lectureId, userContext.hasUserInfo())), + ); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(reloadData, [api, hasUserInfo, pathname]); + + if (!matchedParamsPlayer && !matchedParamsEmbed) + return <Four04 title="Unbekannter Pfad" text="dynamic_player" />; + if (matchedParamsPlayer && matchedParamsEmbed) + throw new Error("Both matchedParamsPlayer and matchedParamsEmbed are true"); + if (!playerData) return <></>; + return ( - <ReloadBoundary reloadFunc={reload}> - <RouterProvider router={router} /> + <ReloadBoundary reloadFunc={reloadData}> + <FallbackErrorBoundary + fallback={(e: any) => ( + <ErrorPage + error={e} + objectName="Vorlesung" + expectedErrorCodes={[ + "unauthorized", + "access_forbidden", + "unknown_object", + "rate_limited", + ]} + /> + )} + > + <Suspense fallback={<div className="spinner-border" />}> + {matchedParamsEmbed && <EmbeddedPlayer playerData={playerData} />} + {matchedParamsPlayer && <Player playerData={playerData} />} + </Suspense> + </FallbackErrorBoundary> </ReloadBoundary> ); }