Skip to content
Snippets Groups Projects
Commit 962b5c75 authored by Simon Künzel's avatar Simon Künzel
Browse files

Add Card and CollapsableCard and use it consistently

parent 6a6f1a0e
No related branches found
No related tags found
No related merge requests found
Showing with 823 additions and 784 deletions
...@@ -8,7 +8,7 @@ import { useApi } from "@/api"; ...@@ -8,7 +8,7 @@ import { useApi } from "@/api";
import { useAuthStatus } from "@/authentication"; import { useAuthStatus } from "@/authentication";
import { ErrorComponent, FallbackErrorBoundary } from "@/error"; import { ErrorComponent, FallbackErrorBoundary } from "@/error";
import { useLanguage } from "@/localization"; import { useLanguage } from "@/localization";
import { ReloadBoundary, semesterToHuman, UpdateOverlay } from "@/miscellaneous"; import { CollapsableCard, ReloadBoundary, semesterToHuman, UpdateOverlay } from "@/miscellaneous";
import { useEditMode, OMCreate, OMEdit, EmbeddedOMFieldComponent } from "@/object_management"; import { useEditMode, OMCreate, OMEdit, EmbeddedOMFieldComponent } from "@/object_management";
type GroupByTypes = "semester" | "full_name" | "organizer" | "topic"; type GroupByTypes = "semester" | "full_name" | "organizer" | "topic";
...@@ -85,16 +85,15 @@ function CourseGroupCard({ ...@@ -85,16 +85,15 @@ function CourseGroupCard({
g, g,
groupedby, groupedby,
onlyShowPublicCourses, onlyShowPublicCourses,
defaultExpanded,
}: { }: {
g: { groupTitle: string; list: Array<course> }; g: { groupTitle: string; list: Array<course> };
groupedby: string; groupedby: string;
onlyShowPublicCourses: boolean; onlyShowPublicCourses: boolean;
defaultExpanded: boolean;
}) { }) {
return ( return (
<div className="card mb-3"> <CollapsableCard header={g.groupTitle} defaultVisible={defaultExpanded}>
<div className="card-body">
<h4 className="card-title">{g.groupTitle}</h4>
<div className="card-text">
<ul className="courses-list m-0 p-0"> <ul className="courses-list m-0 p-0">
{g.list.flatMap((c) => { {g.list.flatMap((c) => {
if ( if (
...@@ -115,9 +114,7 @@ function CourseGroupCard({ ...@@ -115,9 +114,7 @@ function CourseGroupCard({
]; ];
})} })}
</ul> </ul>
</div> </CollapsableCard>
</div>
</div>
); );
} }
...@@ -154,13 +151,17 @@ function CourseList({ ...@@ -154,13 +151,17 @@ function CourseList({
// descending order for semesters // descending order for semesters
return groupBy === "semester" ? -cmp : cmp; return groupBy === "semester" ? -cmp : cmp;
}) })
.map(([key, groupObject]) => { .map(([key, groupObject], index) => {
console.log(index);
return ( return (
<CourseGroupCard <CourseGroupCard
key={key} key={key}
g={groupObject} g={groupObject}
groupedby={groupBy} groupedby={groupBy}
onlyShowPublicCourses={onlyShowPublicCourses} onlyShowPublicCourses={onlyShowPublicCourses}
defaultExpanded={
groupBy !== "semester" || key !== "none" /* Collapse 'Zeitlos' by default */
}
/> />
); );
}); });
......
...@@ -10,6 +10,7 @@ import { LectureCard, LectureLiveLabel } from "@/course"; ...@@ -10,6 +10,7 @@ import { LectureCard, LectureLiveLabel } from "@/course";
import { ErrorComponent, showError, showWarningToast, FallbackErrorBoundary } from "@/error"; import { ErrorComponent, showError, showWarningToast, FallbackErrorBoundary } from "@/error";
import { useLanguage } from "@/localization"; import { useLanguage } from "@/localization";
import { import {
Card,
datetimeToStringOnlyDate, datetimeToStringOnlyDate,
datetimeToStringOnlyTime, datetimeToStringOnlyTime,
ReloadBoundary, ReloadBoundary,
...@@ -56,10 +57,11 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea ...@@ -56,10 +57,11 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
} }
return ( return (
<div className={"card mb-3 " + bgColor}> <Card
<div className="card-body"> objectVisible={data.visible}
<h5 className="card-title d-flex align-items-center"> header={
<div className="overflow-hidden pb-1"> <>
<span className="overflow-hidden">
<EmbeddedOMFieldComponent <EmbeddedOMFieldComponent
object_type="featured" object_type="featured"
object_id={data.id!} object_id={data.id!}
...@@ -68,7 +70,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea ...@@ -68,7 +70,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
initialValue={data.title} initialValue={data.title}
allowMarkdown={true} allowMarkdown={true}
/> />
</div> </span>
{editMode && ( {editMode && (
<EmbeddedOMFieldComponent <EmbeddedOMFieldComponent
object_type="featured" object_type="featured"
...@@ -78,13 +80,13 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea ...@@ -78,13 +80,13 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
initialValue={data.visible} initialValue={data.visible}
/> />
)} )}
<div className="flex-fill" /> <span className="flex-fill" />
{editMode && ( {editMode && (
<div> <span>
<OMHistory object_type="featured" object_id={data.id!} /> <OMHistory object_type="featured" object_id={data.id!} />
<div className="btn-group m-1" role="group"> <span className="btn-group m-1" role="group">
<button <button
type="button" type="button"
className="btn btn-outline-info" className="btn btn-outline-info"
...@@ -101,14 +103,15 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea ...@@ -101,14 +103,15 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
> >
<i className="bi bi-arrow-down" /> <i className="bi bi-arrow-down" />
</button> </button>
</div> </span>
<OMEdit object_type="featured" object_id={data.id!} /> <OMEdit object_type="featured" object_id={data.id!} />
<OMDelete object_type="featured" object_id={data.id!} /> <OMDelete object_type="featured" object_id={data.id!} />
</div> </span>
)} )}
</h5> </>
}
>
{data.type === "image" && data.image_url && ( {data.type === "image" && data.image_url && (
<img <img
src={data.image_url} src={data.image_url}
...@@ -129,17 +132,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea ...@@ -129,17 +132,7 @@ function FeatureCard({ data, all_featured }: { data: featured; all_featured: fea
className="flex-fill" className="flex-fill"
allowMarkdown={true} allowMarkdown={true}
/> />
</Card>
{data.visible === false && (
<>
<hr />
<small className="text-muted">
{language.get("ui.generic.object_invisible")}
</small>
</>
)}
</div>
</div>
); );
} }
...@@ -301,13 +294,13 @@ export default function Home() { ...@@ -301,13 +294,13 @@ export default function Home() {
<Search className="mt-3" queryCallback={updateQueryDeferred} /> <Search className="mt-3" queryCallback={updateQueryDeferred} />
</div> </div>
</div> </div>
<hr />
{query.length > 0 && ( {query.length > 0 && (
<> <>
<SearchResults query={query} />
<hr /> <hr />
<SearchResults query={query} />
</> </>
)} )}
<hr className="mb-0" />
{isPending && <div className="spinner-border" />} {isPending && <div className="spinner-border" />}
{homepageData && ( {homepageData && (
<div className="row"> <div className="row">
...@@ -316,24 +309,12 @@ export default function Home() { ...@@ -316,24 +309,12 @@ export default function Home() {
<FeaturedContent homepageData={homepageData} /> <FeaturedContent homepageData={homepageData} />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<div className="card mb-3"> <Card header={language.get("ui.index.next_recordings")}>
<div className="card-body">
<h5 className="card-title">
{language.get("ui.index.next_recordings")}
</h5>
<UpcomingUploads homepageData={homepageData} /> <UpcomingUploads homepageData={homepageData} />
</div> </Card>
</div> <Card header={language.get("ui.index.recent_videos")}>
<div className="card mb-3">
<div className="card-body">
<h5 className="card-title">
{language.get("ui.index.recent_videos")}
</h5>
<RecentUploads homepageData={homepageData} /> <RecentUploads homepageData={homepageData} />
</div> </Card>
</div>
</div> </div>
</div> </div>
)} )}
......
...@@ -6,7 +6,7 @@ import { useApi, OBJECT_TYPES } from "@/api"; ...@@ -6,7 +6,7 @@ import { useApi, OBJECT_TYPES } from "@/api";
import { ModeratorBarrier } from "@/authentication"; import { ModeratorBarrier } from "@/authentication";
import { showError, ErrorComponent } from "@/error"; import { showError, ErrorComponent } from "@/error";
import { ChooserEditor, StringEditor, IntEditor } from "@/form"; import { ChooserEditor, StringEditor, IntEditor } from "@/form";
import { ReloadBoundary, useReloadBoundary } from "@/miscellaneous"; import { Card, ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management"; import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management";
function ValToStr({ val }: { val: any }) { function ValToStr({ val }: { val: any }) {
...@@ -455,12 +455,10 @@ function ChangelogImpl() { ...@@ -455,12 +455,10 @@ function ChangelogImpl() {
}), }),
); );
return ( return (
<div className="card"> <Card header="Changelog">
<div className="card-header d-flex">Changelog</div>
<div className="card-body">
<p> <p>
Hier werden alle Änderungen an Kursen/Veranstaltungen/Videos etc. geloggt und Hier werden alle Änderungen an Kursen/Veranstaltungen/Videos etc. geloggt und können
können Rückgängig gemacht werden. Rückgängig gemacht werden.
</p> </p>
<ReloadBoundary reloadFunc={reloadData}> <ReloadBoundary reloadFunc={reloadData}>
<div className="d-flex"> <div className="d-flex">
...@@ -510,8 +508,7 @@ function ChangelogImpl() { ...@@ -510,8 +508,7 @@ function ChangelogImpl() {
data && <ChangelogList changelog={data} setFilter={setFilter} /> data && <ChangelogList changelog={data} setFilter={setFilter} />
)} )}
</ReloadBoundary> </ReloadBoundary>
</div> </Card>
</div>
); );
} }
......
...@@ -7,7 +7,7 @@ import type { GetNewConfigurationResponse, course } from "@/api/types"; ...@@ -7,7 +7,7 @@ import type { GetNewConfigurationResponse, course } from "@/api/types";
import { useApi } from "@/api"; import { useApi } from "@/api";
import { ModeratorBarrier } from "@/authentication"; import { ModeratorBarrier } from "@/authentication";
import { showError, showErrorToast } from "@/error"; import { showError, showErrorToast } from "@/error";
import { ICalEvent, parseICal, ReloadBoundary, useReloadBoundary } from "@/miscellaneous"; import { Card, ICalEvent, parseICal, ReloadBoundary, useReloadBoundary } from "@/miscellaneous";
function TerminBody({ function TerminBody({
course, course,
...@@ -260,10 +260,10 @@ function TerminBody({ ...@@ -260,10 +260,10 @@ function TerminBody({
<div className="spinner-border mb-2" role="status" /> <div className="spinner-border mb-2" role="status" />
) : null} ) : null}
<div className="card mb-2"> <Card
<div className="card-header d-flex align-items-center"> header={
<>
<span className="flex-grow-1">Im Import, nicht bei uns</span> <span className="flex-grow-1">Im Import, nicht bei uns</span>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={importAllEvents} onClick={importAllEvents}
...@@ -271,8 +271,9 @@ function TerminBody({ ...@@ -271,8 +271,9 @@ function TerminBody({
> >
Alle anlegen Alle anlegen
</button> </button>
</div> </>
<div className="card-body"> }
>
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
...@@ -308,12 +309,12 @@ function TerminBody({ ...@@ -308,12 +309,12 @@ function TerminBody({
))} ))}
</tbody> </tbody>
</table> </table>
</div> </Card>
</div>
<div className="card">
<div className="card-header d-flex align-items-center">
<span className="flex-grow-1">Bei uns, nicht im Import</span>
<Card
header={
<>
<span className="flex-grow-1">Bei uns, nicht im Import</span>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={removeAllEvents} onClick={removeAllEvents}
...@@ -321,8 +322,9 @@ function TerminBody({ ...@@ -321,8 +322,9 @@ function TerminBody({
> >
Alle entfernen Alle entfernen
</button> </button>
</div> </>
<div className="card-body"> }
>
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
...@@ -358,8 +360,7 @@ function TerminBody({ ...@@ -358,8 +360,7 @@ function TerminBody({
))} ))}
</tbody> </tbody>
</table> </table>
</div> </Card>
</div>
</div> </div>
); );
} }
...@@ -469,9 +470,7 @@ function RWTHOnlineImportImpl() { ...@@ -469,9 +470,7 @@ function RWTHOnlineImportImpl() {
<Link href={`/${course.handle}`} className="btn btn-primary mb-2"> <Link href={`/${course.handle}`} className="btn btn-primary mb-2">
<span className="bi bi-chevron-left" /> Zum Kurs <span className="bi bi-chevron-left" /> Zum Kurs
</Link> </Link>
<div className="card mb-2"> <Card header="Import von RWTHOnline">
<div className="card-header d-flex">Import von RWTHOnline</div>
<div className="card-body">
<a <a
href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche" href="https://online.rwth-aachen.de/RWTHonline/pl/ui/%24ctx/wbSuche.LVSuche"
target="_blank" target="_blank"
...@@ -489,11 +488,7 @@ function RWTHOnlineImportImpl() { ...@@ -489,11 +488,7 @@ function RWTHOnlineImportImpl() {
{isImporting ? ( {isImporting ? (
<div className="spinner-border mt-2" role="status" /> <div className="spinner-border mt-2" role="status" />
) : ( ) : (
<button <button className="btn btn-primary mt-2" type="button" onClick={onImport}>
className="btn btn-primary mt-2"
type="button"
onClick={onImport}
>
Zum Import hinzufügen Zum Import hinzufügen
</button> </button>
)} )}
...@@ -520,8 +515,7 @@ function RWTHOnlineImportImpl() { ...@@ -520,8 +515,7 @@ function RWTHOnlineImportImpl() {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </Card>
</div>
<TerminBody course={course} imported_events={imported_events} /> <TerminBody course={course} imported_events={imported_events} />
</ReloadBoundary> </ReloadBoundary>
); );
......
...@@ -3,7 +3,7 @@ import { useApi, JOB_STATES } from "@/api"; ...@@ -3,7 +3,7 @@ import { useApi, JOB_STATES } from "@/api";
import { ModeratorBarrier } from "@/authentication"; import { ModeratorBarrier } from "@/authentication";
import { ErrorComponent } from "@/error"; import { ErrorComponent } from "@/error";
import { StringEditor, ChooserEditor } from "@/form"; import { StringEditor, ChooserEditor } from "@/form";
import { ReloadBoundary, ExpandableString, datetimeToString, Spinner } from "@/miscellaneous"; import { Card, ReloadBoundary, ExpandableString, datetimeToString, Spinner } from "@/miscellaneous";
import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management"; import { useFilteredDataView, PagingNavigation, FilterInput } from "@/object_management";
function JobList({ jobsResp }: { jobsResp: GetJobsResponse }) { function JobList({ jobsResp }: { jobsResp: GetJobsResponse }) {
...@@ -113,9 +113,7 @@ function JobsImpl() { ...@@ -113,9 +113,7 @@ function JobsImpl() {
}), }),
); );
return ( return (
<div className="card"> <Card header="Jobs">
<div className="card-header d-flex">Jobs</div>
<div className="card-body">
<p>Hier werden alle Jobs angezeigt</p> <p>Hier werden alle Jobs angezeigt</p>
<ReloadBoundary reloadFunc={reloadData}> <ReloadBoundary reloadFunc={reloadData}>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
...@@ -157,8 +155,7 @@ function JobsImpl() { ...@@ -157,8 +155,7 @@ function JobsImpl() {
data && <JobList jobsResp={data} /> data && <JobList jobsResp={data} />
)} )}
</ReloadBoundary> </ReloadBoundary>
</div> </Card>
</div>
); );
} }
......
...@@ -7,7 +7,13 @@ import { ModeratorBarrier } from "@/authentication"; ...@@ -7,7 +7,13 @@ import { ModeratorBarrier } from "@/authentication";
import { showError, ErrorComponent } from "@/error"; import { showError, ErrorComponent } from "@/error";
import { BooleanEditor } from "@/form"; import { BooleanEditor } from "@/form";
import { JobStatusCard } from "@/job"; import { JobStatusCard } from "@/job";
import { ReloadBoundary, ExpandableString, datetimeToString, showInfoToast } from "@/miscellaneous"; import {
Card,
ReloadBoundary,
ExpandableString,
datetimeToString,
showInfoToast,
} from "@/miscellaneous";
import { import {
useFilteredDataView, useFilteredDataView,
PagingNavigation, PagingNavigation,
...@@ -141,9 +147,7 @@ function SorterFilesImpl() { ...@@ -141,9 +147,7 @@ function SorterFilesImpl() {
}; };
return ( return (
<div className="card"> <Card header="Sorter Files">
<div className="card-header d-flex">Sorter Files</div>
<div className="card-body">
<div className="d-flex align-items-center mb-4"> <div className="d-flex align-items-center mb-4">
<button <button
onClick={runSourceFileSorter} onClick={runSourceFileSorter}
...@@ -183,17 +187,12 @@ function SorterFilesImpl() { ...@@ -183,17 +187,12 @@ function SorterFilesImpl() {
{isLoading && <div className="spinner-border ms-3 mt-1" />} {isLoading && <div className="spinner-border ms-3 mt-1" />}
</div> </div>
{error ? ( {error ? (
<ErrorComponent <ErrorComponent error={error} objectName="Sorter Files" showButtons={false} />
error={error}
objectName="Sorter Files"
showButtons={false}
/>
) : ( ) : (
data && <SorterFileList sorterFilesResp={data} /> data && <SorterFileList sorterFilesResp={data} />
)} )}
</ReloadBoundary> </ReloadBoundary>
</div> </Card>
</div>
); );
} }
......
...@@ -2,6 +2,7 @@ import Link from "next/link"; ...@@ -2,6 +2,7 @@ import Link from "next/link";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { ModeratorBarrier } from "@/authentication"; import { ModeratorBarrier } from "@/authentication";
import { Card } from "@/miscellaneous";
interface Days { interface Days {
index: number; index: number;
...@@ -168,14 +169,11 @@ function TimetableTable() { ...@@ -168,14 +169,11 @@ function TimetableTable() {
function TimetableImpl() { function TimetableImpl() {
return ( return (
<div className="card"> <Card header="Timetable">
<div className="card-header d-flex">Timetable</div>
<div className="card-body">
This page is a work in progress, the data here is not real. This page is a work in progress, the data here is not real.
<hr /> <hr />
<TimetableTable /> <TimetableTable />
</div> </Card>
</div>
/* TODO: pagination /* TODO: pagination
<div className="hidden-print"> <div className="hidden-print">
<div style="margin-top: 10px; padding: 15px;" className="col-xs-12"> <div style="margin-top: 10px; padding: 15px;" className="col-xs-12">
......
...@@ -99,6 +99,14 @@ html:has(> body.modal-open) { ...@@ -99,6 +99,14 @@ html:has(> body.modal-open) {
background-color: inherit; // this fixes tables messing up background colors of parent elements background-color: inherit; // this fixes tables messing up background colors of parent elements
} }
.card-header:not(:has(+ .card-body)) {
border-bottom: 0;
}
.card-header {
--bs-card-cap-padding-y: 0.6em;
}
.icon-rwth { .icon-rwth {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
......
...@@ -3,7 +3,7 @@ import Link from "next/link"; ...@@ -3,7 +3,7 @@ import Link from "next/link";
import type { GetCourseResponse } from "@/api/types"; import type { GetCourseResponse } from "@/api/types";
import { useAuthStatus, AuthenticationMethodIcons } from "@/authentication"; import { useAuthStatus, AuthenticationMethodIcons } from "@/authentication";
import { useLanguage } from "@/localization"; import { useLanguage } from "@/localization";
import { Title, UpdateOverlay } from "@/miscellaneous"; import { Card, Title, UpdateOverlay } from "@/miscellaneous";
import { import {
useEditMode, useEditMode,
OMCreate, OMCreate,
...@@ -23,9 +23,10 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { ...@@ -23,9 +23,10 @@ function ListingHeader({ course }: { course: GetCourseResponse }) {
const { language } = useLanguage(); const { language } = useLanguage();
return ( return (
<div className={`card mb-3 ${course.visible === false ? "bg-danger-subtle" : ""}`}> <Card
<div className="card-body"> objectVisible={course.visible}
<h5 className="card-title d-flex"> header={
<>
<span className="panel-title flex-fill"> <span className="panel-title flex-fill">
<EmbeddedOMFieldComponent <EmbeddedOMFieldComponent
object_type="course" object_type="course"
...@@ -58,13 +59,12 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { ...@@ -58,13 +59,12 @@ function ListingHeader({ course }: { course: GetCourseResponse }) {
authentication_methods={course.default_authentication_methods} authentication_methods={course.default_authentication_methods}
/> />
</div> </div>
</h5> </>
}
>
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<Link <Link href={`/courses#course-${course.id}`} className="btn btn-primary mb-1">
href={`/courses#course-${course.id}`}
className="btn btn-primary mb-1"
>
<span className="bi bi-chevron-left" />{" "} <span className="bi bi-chevron-left" />{" "}
{language.get("ui.course.back_to_list")} {language.get("ui.course.back_to_list")}
</Link> </Link>
...@@ -134,16 +134,7 @@ function ListingHeader({ course }: { course: GetCourseResponse }) { ...@@ -134,16 +134,7 @@ function ListingHeader({ course }: { course: GetCourseResponse }) {
<DownloadAllModal course={course} /> <DownloadAllModal course={course} />
</div> </div>
</div> </div>
{course.visible === false && ( </Card>
<>
<hr />
<small className="text-muted">
{language.get("ui.generic.object_invisible")}
</small>
</>
)}
</div>
</div>
); );
} }
...@@ -151,9 +142,9 @@ function ListingBody({ course }: { course: GetCourseResponse }) { ...@@ -151,9 +142,9 @@ function ListingBody({ course }: { course: GetCourseResponse }) {
const { editMode } = useEditMode(); const { editMode } = useEditMode();
return ( return (
<div className="card mb-3"> <Card
<div className="card-body"> header={
<h5 className="card-title d-flex align-items-center"> <>
Videos Videos
<div className="flex-fill" /> <div className="flex-fill" />
{editMode && ( {editMode && (
...@@ -177,7 +168,9 @@ function ListingBody({ course }: { course: GetCourseResponse }) { ...@@ -177,7 +168,9 @@ function ListingBody({ course }: { course: GetCourseResponse }) {
</Link> </Link>
</> </>
)} )}
</h5> </>
}
>
<ul className="list-group lectureslist"> <ul className="list-group lectureslist">
{course.lectures!.map((lecture) => ( {course.lectures!.map((lecture) => (
<LectureListItem <LectureListItem
...@@ -188,8 +181,7 @@ function ListingBody({ course }: { course: GetCourseResponse }) { ...@@ -188,8 +181,7 @@ function ListingBody({ course }: { course: GetCourseResponse }) {
/> />
))} ))}
</ul> </ul>
</div> </Card>
</div>
); );
} }
......
...@@ -7,8 +7,9 @@ import { showError, ErrorComponent } from "@/error"; ...@@ -7,8 +7,9 @@ import { showError, ErrorComponent } from "@/error";
import { JobStatusCard } from "@/job"; import { JobStatusCard } from "@/job";
import { useLanguage } from "@/localization"; import { useLanguage } from "@/localization";
import { import {
Card,
Spinner, Spinner,
ReloadBoundary, NestedReloadBoundary,
useMutexCall, useMutexCall,
showInfoToast, showInfoToast,
TooltipButton, TooltipButton,
...@@ -390,9 +391,10 @@ function MediumFileListItem({ ...@@ -390,9 +391,10 @@ function MediumFileListItem({
} }
return ( return (
<div className={"card mb-3 " + (file.to_be_replaced ? "bg-warning-subtle" : "")}> <Card
<div id={`medium_file_${file.id}`} className="card-body"> header={`Medium File ${file.id}`}
<h5>Medium File {file.id}</h5> className={file.to_be_replaced ? "bg-warning-subtle" : ""}
>
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
...@@ -451,8 +453,7 @@ function MediumFileListItem({ ...@@ -451,8 +453,7 @@ function MediumFileListItem({
</table> </table>
{metadataComp} {metadataComp}
{publishMediumComp} {publishMediumComp}
</div> </Card>
</div>
); );
} }
...@@ -466,9 +467,7 @@ function SorterFileListItem({ ...@@ -466,9 +467,7 @@ function SorterFileListItem({
const file = context.sorter_files[sorterFileId.toString()]!; const file = context.sorter_files[sorterFileId.toString()]!;
return ( return (
<div className="card mb-3"> <Card header={`Sorter File ${file.id}`}>
<div className="card-body">
<h5>Sorter File {file.id}</h5>
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
...@@ -498,11 +497,10 @@ function SorterFileListItem({ ...@@ -498,11 +497,10 @@ function SorterFileListItem({
Designated Medium File ID Designated Medium File ID
<TooltipButton> <TooltipButton>
Shows which medium file was created for this source file. That Shows which medium file was created for this source file. That
medium file and this source file reference the same file (have medium file and this source file reference the same file (have the
the same file path). same file path).
<br /> <br />
Note that this medium file can change if the process changes, Note that this medium file can change if the process changes, etc.
etc.
</TooltipButton> </TooltipButton>
</td> </td>
<td> <td>
...@@ -515,8 +513,7 @@ function SorterFileListItem({ ...@@ -515,8 +513,7 @@ function SorterFileListItem({
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </Card>
</div>
); );
} }
...@@ -612,10 +609,11 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) { ...@@ -612,10 +609,11 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) {
<td> <td>
Is automatic Media Process Scheduler enabled?{" "} Is automatic Media Process Scheduler enabled?{" "}
<TooltipButton> <TooltipButton>
This shows whether the media process scheduler will automatically This shows whether the media process scheduler will
run when any changes occur that might require processing media automatically run when any changes occur that might require
files. This can be changed in the lecture config or, if the lecture processing media files. This can be changed in the lecture
has specified &apos;Inherit&apos;, in the course config. config or, if the lecture has specified &apos;Inherit&apos;, in
the course config.
</TooltipButton> </TooltipButton>
</td> </td>
<td> <td>
...@@ -635,15 +633,15 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) { ...@@ -635,15 +633,15 @@ export function MediaProcessOverview({ lectureId }: { lectureId: int }) {
Run Media Process Scheduler Manually Run Media Process Scheduler Manually
</button> </button>
<TooltipButton> <TooltipButton>
If the automatic media process scheduler is disabled, you can run it manually If the automatic media process scheduler is disabled, you can run it
here (You will need to run it multiple times until the process is finished; manually here (You will need to run it multiple times until the process is
after every finished job). finished; after every finished job).
<br /> <br />
<br /> <br />
If a job failed the automatic process scheduler will also not run again If a job failed the automatic process scheduler will also not run again
automatically (to prevent infinite loops) and you can run it manually. It will automatically (to prevent infinite loops) and you can run it manually. It
automatically detect the failed job and schedule a new one which might or might will automatically detect the failed job and schedule a new one which might
not fail again. or might not fail again.
<br /> <br />
<br /> <br />
In all other cases you do not need to run it manually. In all other cases you do not need to run it manually.
......
...@@ -14,7 +14,7 @@ import type { course, chapter, publish_medium, lecture } from "@/api/types"; ...@@ -14,7 +14,7 @@ import type { course, chapter, publish_medium, lecture } from "@/api/types";
import { useApi } from "@/api"; import { useApi } from "@/api";
import { ViewPermissionAuthorization } from "@/authentication"; import { ViewPermissionAuthorization } from "@/authentication";
import { useLanguage } from "@/localization"; import { useLanguage } from "@/localization";
import { Title, UpdateOverlay, showInfoToast, parseApiDatetime } from "@/miscellaneous"; import { Card, Title, UpdateOverlay, showInfoToast, parseApiDatetime } from "@/miscellaneous";
import { import {
useEditMode, useEditMode,
OMDelete, OMDelete,
...@@ -24,7 +24,7 @@ import { ...@@ -24,7 +24,7 @@ import {
} from "@/object_management"; } from "@/object_management";
import { basePath } from "#basepath"; import { basePath } from "#basepath";
import { LectureCard, urlForLecture, getLectureThumbnailUrlNoPlaceholder } from "./Lecture"; import { LectureCard, getLectureThumbnailUrlNoPlaceholder } from "./Lecture";
import { import {
PublishMediumDownloadButton, PublishMediumDownloadButton,
getSortedPlayerPublishMedia, getSortedPlayerPublishMedia,
...@@ -237,8 +237,34 @@ function VideoPlayer({ ...@@ -237,8 +237,34 @@ function VideoPlayer({
); );
} }
function LectureSuggestions({ course, lecture }: { course: course; lecture: lecture }) { function LectureSuggestionCard({
course,
lecture,
isPrevious,
}: {
course: course;
lecture: lecture;
isPrevious: boolean;
}) {
const { language } = useLanguage(); const { language } = useLanguage();
return (
<Card
objectVisible={lecture.visible}
headerNoStyling={true}
header={
<span className="d-flex justify-content-center">
{isPrevious && <i className="bi bi-arrow-left-circle me-2" />}
{language.get(`ui.video_player.${isPrevious ? "previous" : "next"}_lecture`)}
{!isPrevious && <i className="bi bi-arrow-right-circle ms-2" />}
</span>
}
>
<LectureCard course={course} lecture={lecture} size="small" />
</Card>
);
}
function LectureSuggestions({ course, lecture }: { course: course; lecture: lecture }) {
const prevLecture = course.lectures?.findLast( const prevLecture = course.lectures?.findLast(
(l) => (l) =>
parseApiDatetime(l.time) < parseApiDatetime(lecture.time) && parseApiDatetime(l.time) < parseApiDatetime(lecture.time) &&
...@@ -253,44 +279,11 @@ function LectureSuggestions({ course, lecture }: { course: course; lecture: lect ...@@ -253,44 +279,11 @@ function LectureSuggestions({ course, lecture }: { course: course; lecture: lect
return ( return (
<div className="d-flex w-100 flex-column flex-sm-row"> <div className="d-flex w-100 flex-column flex-sm-row">
{prevLecture && ( {prevLecture && (
<div className={`card ${prevLecture.visible === false ? "bg-danger-subtle" : ""}`}> <LectureSuggestionCard course={course} lecture={prevLecture} isPrevious={true} />
<Link
className="card-header text-center bg-light-subtle"
href={urlForLecture(course.handle, prevLecture.id)}
>
<i className="bi bi-arrow-left-circle me-2" />
{language.get("ui.video_player.previous_lecture")}
</Link>
<div className="card-body">
<LectureCard course={course} lecture={prevLecture} size="small" />
</div>
{prevLecture.visible === false && (
<div className="card-footer text-center text-muted">
{language.get("ui.generic.object_invisible")}
</div>
)}
</div>
)} )}
<div className="flex-fill p-2" /> <div className="flex-fill p-2" />
{nextLecture && ( {nextLecture && (
<div className={`card ${nextLecture.visible === false ? "bg-danger-subtle" : ""}`}> <LectureSuggestionCard course={course} lecture={nextLecture} isPrevious={false} />
<Link
className="card-header text-center bg-light-subtle"
href={urlForLecture(course.handle, nextLecture.id)}
>
{language.get("ui.video_player.next_lecture")}
<i className="bi bi-arrow-right-circle ms-2" />
</Link>
<div className="card-body">
<LectureCard course={course} lecture={nextLecture} size="small" />
</div>
{nextLecture.visible === false && (
<div className="card-footer text-center text-muted">
{language.get("ui.generic.object_invisible")}
</div>
)}
</div>
)} )}
</div> </div>
); );
...@@ -473,9 +466,11 @@ export default function Player({ ...@@ -473,9 +466,11 @@ export default function Player({
</Head> </Head>
<Title title={`${course.short_name} - ${lecture.title}`} /> <Title title={`${course.short_name} - ${lecture.title}`} />
<UpdateOverlay show={disabled} /> <UpdateOverlay show={disabled} />
<div className={`card ${lecture.visible === false ? "bg-danger-subtle" : ""}`}> <Card
<div className="card-header d-flex"> objectVisible={lecture.visible}
<div className="flex-fill align-self-center"> header={
<>
<span>
<strong> <strong>
<Link href={`/${course.handle}#lecture-${lecture.id}`}> <Link href={`/${course.handle}#lecture-${lecture.id}`}>
{course.full_name} {course.full_name}
...@@ -498,9 +493,9 @@ export default function Player({ ...@@ -498,9 +493,9 @@ export default function Player({
initialValue={lecture.time} initialValue={lecture.time}
/> />
) )
</div> </span>
<span className="flex-fill" />
<div>
{editMode && ( {editMode && (
<> <>
<EmbeddedOMFieldComponent <EmbeddedOMFieldComponent
...@@ -516,9 +511,9 @@ export default function Player({ ...@@ -516,9 +511,9 @@ export default function Player({
<OMDelete object_type="lecture" object_id={lecture.id} /> <OMDelete object_type="lecture" object_id={lecture.id} />
</> </>
)} )}
</div> </>
</div> }
<div className="card-body"> >
<div className="row p-0 mt-1"> <div className="row p-0 mt-1">
<div className="col-12 pb-4"> <div className="col-12 pb-4">
<Link <Link
...@@ -551,17 +546,7 @@ export default function Player({ ...@@ -551,17 +546,7 @@ export default function Player({
<Chapters chapters={lecture.chapters} seekTo={seekTo} /> <Chapters chapters={lecture.chapters} seekTo={seekTo} />
<LectureSuggestions course={course} lecture={lecture} /> <LectureSuggestions course={course} lecture={lecture} />
</Card>
{lecture.visible === false && (
<>
<hr />
<small className="text-muted">
{language.get("ui.generic.object_invisible")}
</small>
</>
)}
</div>
</div>
</> </>
); );
} }
...@@ -2,7 +2,13 @@ import { useState, useEffect, useRef } from "react"; ...@@ -2,7 +2,13 @@ import { useState, useEffect, useRef } from "react";
import type { int, job, job_state } from "@/api/types"; import type { int, job, job_state } from "@/api/types";
import { useApi } from "@/api"; import { useApi } from "@/api";
import { datetimeToString, Spinner, useDebouncedCall, useReloadBoundary } from "@/miscellaneous"; import {
Card,
datetimeToString,
Spinner,
useDebouncedCall,
useReloadBoundary,
} from "@/miscellaneous";
export function JobStatusCard({ jobId }: { jobId: int }) { export function JobStatusCard({ jobId }: { jobId: int }) {
const api = useApi(); const api = useApi();
...@@ -73,12 +79,14 @@ export function JobStatusCard({ jobId }: { jobId: int }) { ...@@ -73,12 +79,14 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
}, []); }, []);
return ( return (
<div className="card"> <Card
<span className="card-header d-flex align-items-center"> header={
<>
Job {jobId} Job {jobId}
<Spinner visible={isLoading} /> <Spinner visible={isLoading} />
</span> </>
<div className="card-body"> }
>
{job !== undefined && ( {job !== undefined && (
<table className="table"> <table className="table">
<tbody> <tbody>
...@@ -96,9 +104,7 @@ export function JobStatusCard({ jobId }: { jobId: int }) { ...@@ -96,9 +104,7 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
</tr> </tr>
<tr> <tr>
<td>Run Start Time</td> <td>Run Start Time</td>
<td> <td>{job.run_start_time && datetimeToString(job.run_start_time)}</td>
{job.run_start_time && datetimeToString(job.run_start_time)}
</td>
</tr> </tr>
<tr> <tr>
<td>Run End Time</td> <td>Run End Time</td>
...@@ -108,7 +114,6 @@ export function JobStatusCard({ jobId }: { jobId: int }) { ...@@ -108,7 +114,6 @@ export function JobStatusCard({ jobId }: { jobId: int }) {
</table> </table>
)} )}
{errorMsg && <div className="text-warning">{errorMsg}</div>} {errorMsg && <div className="text-warning">{errorMsg}</div>}
</div> </Card>
</div>
); );
} }
import React from "react";
import { useState } from "react";
import { useLanguage } from "@/localization";
export function Card({
header,
headerNoStyling,
className,
bodyVisible,
objectVisible,
children,
}: {
header: any;
headerNoStyling?: boolean;
className?: string;
bodyVisible?: boolean;
objectVisible?: boolean;
children: React.ReactNode;
}) {
const { language } = useLanguage();
return (
<div
className={
"card mt-3 mb-3 " +
((objectVisible ?? true) ? "" : "bg-danger-subtle ") +
(className ?? "")
}
>
{(headerNoStyling ?? false) ? (
<span className="card-header">{header}</span>
) : (
<h5 className="card-header d-flex align-items-center">{header}</h5>
)}
{(bodyVisible ?? true) && <div className="card-body">{children}</div>}
{!(objectVisible ?? true) && (
<div className="card-footer text-center text-muted">
{language.get("ui.generic.object_invisible")}
</div>
)}
</div>
);
}
export function CollapsableCard({
header,
headerNoStyling,
defaultVisible,
className,
objectVisible,
children,
}: {
header: any;
headerNoStyling?: boolean;
defaultVisible: boolean;
className?: string;
objectVisible?: boolean;
children: React.ReactNode;
}) {
const [visible, setVisible] = useState<boolean>(defaultVisible);
return (
<Card
className={className}
headerNoStyling={headerNoStyling}
bodyVisible={visible}
objectVisible={objectVisible}
header={
<>
{header}
<button
className="btn btn-outline-secondary border-0 ms-1"
onClick={() => setVisible((old) => !old)}
>
<i className={"bi bi-chevron-" + (visible ? "down" : "up")} />
</button>
</>
}
>
{children}
</Card>
);
}
...@@ -8,6 +8,7 @@ export { ...@@ -8,6 +8,7 @@ export {
datetimeToString, datetimeToString,
filesizeToHuman, filesizeToHuman,
} from "./Formatting"; } from "./Formatting";
export { Card, CollapsableCard } from "./Card";
export { export {
useDebounce, useDebounce,
useDebounceWithArgument, useDebounceWithArgument,
......
...@@ -308,7 +308,7 @@ export function EmbeddedOMFieldComponent({ ...@@ -308,7 +308,7 @@ export function EmbeddedOMFieldComponent({
className={ className={
"h-auto d-inline-flex align-middle" + "h-auto d-inline-flex align-middle" +
className + className +
` ${isEditing ? "w-100" : ""} ${changeIndicator === "background" ? "p-2 rounded" : ""}` ` ${isEditing && field_type !== "datetime" ? "w-100" : ""} ${changeIndicator === "background" ? "p-2 rounded" : ""}`
} }
style={{ style={{
backgroundColor: backgroundColor:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment