Skip to content

Commit

Permalink
Refactor manage code and add series details page
Browse files Browse the repository at this point in the history
This generalizes the `Nav` and `Details` code that
was used for videos, and repurposes it for series
as well.

With these changes, it should also be fairly
easy to add this for playlists later on.
  • Loading branch information
owi92 committed Jan 27, 2025
1 parent 960f62a commit 8c2b5a1
Show file tree
Hide file tree
Showing 13 changed files with 598 additions and 823 deletions.
13 changes: 9 additions & 4 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ login-page:
video:
video: Video
video-page: 'Video Seite: „{{video}}“'
created: Erstellt
updated: Zuletzt verändert
duration: Abspielzeit
link: Zur Videoseite
part-of-series: Teil der Serie
Expand Down Expand Up @@ -442,6 +440,15 @@ manage:
no-entries-found: Keine Einträge gefunden.
missing-date: Unbekannt

shared:
created: Erstellt
updated: Zuletzt verändert
details:
share-direct-link: Via Direktlink teilen
copy-direct-link-to-clipboard: Videolink in Zwischenablage kopieren
acl:
title: Access policy

my-series:
title: Meine Serien
content: Inhalt
Expand All @@ -451,9 +458,7 @@ manage:
title: Meine Videos
details:
title: Videodetails
share-direct-link: Via Direktlink teilen
set-time: 'Starten bei: '
copy-direct-link-to-clipboard: Videolink in Zwischenablage kopieren
open-in-editor: Im Videoeditor öffnen
referencing-pages: Referenzierende Seiten
referencing-pages-explanation: 'Dieses Video wird von den folgenden Seiten referenziert:'
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ login-page:
video:
video: Video
video-page: 'Video page: “{{video}}”'
created: Created
updated: Last updated
duration: Duration
link: Go to video page
part-of-series: Part of series
Expand Down Expand Up @@ -198,6 +196,7 @@ playlist:

series:
series: Series
series-page: 'Series page: “{{series}}”'
deleted: Deleted series
deleted-series-block: The series referenced here was deleted.
entry-of-series-thumbnail: "Thumbnail for entry of “{{series}}”"
Expand Down Expand Up @@ -423,18 +422,27 @@ manage:
no-entries-found: No entries found.
missing-date: Unknown

shared:
created: Created
updated: Last updated
details:
share-direct-link: Share via direct link
copy-direct-link-to-clipboard: Copy link to clipboard
acl:
title: Access policy

my-series:
title: My series
content: Content
no-of-videos: '{{count}} videos'
details:
title: Series details

my-videos:
title: My videos
details:
title: Video details
share-direct-link: Share via direct link
set-time: 'Start at: '
copy-direct-link-to-clipboard: Copy video link to clipboard
open-in-editor: Open in video editor
referencing-pages: Referencing pages
referencing-pages-explanation: 'This video is referenced on the following pages:'
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import React from "react";
import { ManageVideoAccessRoute } from "./routes/manage/Video/Access";
import { DirectPlaylistOCRoute, DirectPlaylistRoute } from "./routes/Playlist";
import { ManageSeriesRoute } from "./routes/manage/Series";
import { ManageSeriesDetailsRoute } from "./routes/manage/Series/Details";



Expand Down Expand Up @@ -67,6 +68,7 @@ const {
ManageVideoTechnicalDetailsRoute,
ManageRealmRoute,
ManageSeriesRoute,
ManageSeriesDetailsRoute,
UploadRoute,
AddChildRoute,
ManageRealmContentRoute,
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
return <>
<div>
<CopyableInput
label={t("manage.my-videos.details.copy-direct-link-to-clipboard")}
label={t("manage.shared.details.copy-direct-link-to-clipboard")}
css={{ fontSize: 14, width: 400, marginBottom: 6 }}
value={url}
/>
Expand Down Expand Up @@ -1285,7 +1285,7 @@ const VideoDate: React.FC<VideoDateProps> = ({ event }) => {
}
{updatedFull && <>
<br/>
<i>{t("video.updated")}</i>: {updatedFull}
<i>{t("manage.shared.updated")}</i>: {updatedFull}
</>}
</>;

Expand All @@ -1307,11 +1307,11 @@ const VideoDate: React.FC<VideoDateProps> = ({ event }) => {
tooltip = <>
{startedDate
? <><i>{t("video.started")}</i>: {startFull}</>
: <><i>{t("video.created")}</i>: {createdFull}</>
: <><i>{t("manage.shared.created")}</i>: {createdFull}</>
}
{updatedFull && <>
<br/>
<i>{t("video.updated")}</i>: {updatedFull}
<i>{t("manage.shared.updated")}</i>: {updatedFull}
</>}
</>;

Expand Down
32 changes: 32 additions & 0 deletions frontend/src/routes/manage/Series/Details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import i18n from "../../../i18n";
import { makeManageSeriesRoute } from "./Shared";
import { ManageSeriesRoute } from ".";
import { DirectSeriesRoute } from "../../Series";
import { DetailsPage, UpdatedCreatedInfo, DirectLink, MetadataSection } from "../Shared/Details";


export const ManageSeriesDetailsRoute = makeManageSeriesRoute(
"details",
"",
series => <DetailsPage
pageTitle="manage.my-series.details.title"
asset={{
...series,
description: series.syncedData?.description,
urlProps: {
url: new URL(DirectSeriesRoute.url({ seriesId: series.id }), document.baseURI),
},
}}
breadcrumb={{
label: i18n.t("manage.my-series.title"),
link: ManageSeriesRoute.url,
}}
sections={series => [
<UpdatedCreatedInfo key="created-info" asset={series} />,
<DirectLink key="direct-link" asset={series} />,
<div key="metadata" css={{ marginBottom: 32 }}>
<MetadataSection asset={series} />
</div>,
]}
/>,
);
146 changes: 146 additions & 0 deletions frontend/src/routes/manage/Series/Shared.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useTranslation } from "react-i18next";
import { LuShieldCheck, LuPenLine, LuEye } from "react-icons/lu";
import { graphql } from "react-relay";

import { RootLoader } from "../../../layout/Root";
import { makeRoute, Route } from "../../../rauta";
import { loadQuery } from "../../../relay";
import { NotFound } from "../../NotFound";
import { b64regex } from "../../util";
import { seriesId, keyOfId } from "../../../util";
import CONFIG from "../../../config";
import { ManageSeriesRoute, SeriesThumbnail } from ".";
import { SharedSeriesManageQuery } from "./__generated__/SharedSeriesManageQuery.graphql";
import { DirectSeriesRoute } from "../../Series";
import { BackLink, ManageAssetNav, SharedManageAssetNavProps } from "../Shared/Nav";
import { COLORS } from "../../../color";


export const PAGE_WIDTH = 1100;

export type QueryResponse = SharedSeriesManageQuery["response"];
export type Series = NonNullable<QueryResponse["series"]>;

type ManageSeriesSubPageType = "details" | "acl";

/** Helper around `makeRoute` for manage single series subpages. */
export const makeManageSeriesRoute = (
page: ManageSeriesSubPageType,
path: string,
render: (series: Series, data: QueryResponse) => JSX.Element,
): Route & { url: (args: { seriesId: string }) => string } => (
makeRoute({
url: ({ seriesId }: { seriesId: string }) => `/~manage/series/${keyOfId(seriesId)}/${path}`,
match: url => {
const regex = new RegExp(`^/~manage/series/(${b64regex}+)${path}/?$`, "u");
const params = regex.exec(url.pathname);
if (params === null) {
return null;
}

const id = decodeURIComponent(params[1]);
const queryRef = loadQuery<SharedSeriesManageQuery>(query, {
id: seriesId(id),
});

return {
render: () => <RootLoader
{...{ query, queryRef }}
noindex
nav={data => data.series ? [
<BackLink
key={1}
url={ManageSeriesRoute.url}
title="manage.my-series.title"
/>,
<ManageSeriesNav key={2} series={data.series} active={page} />,
] : []}
render={data => {
if (data.series == null) {
return <NotFound kind="series" />;
}
return render(data.series, data);
}}
/>,
dispose: () => queryRef.dispose(),
};
},
})
);


const query = graphql`
query SharedSeriesManageQuery($id: ID!) {
...UserData
...AccessKnownRolesData
series: seriesById(id: $id) {
id
title
created
updated
syncedData { description }
entries {
__typename
...on AuthorizedEvent {
isLive
syncedData { thumbnail audioOnly }
}
}
}
}
`;


type ManageSeriesNavProps = SharedManageAssetNavProps & {
series: Series;
};

const ManageSeriesNav: React.FC<ManageSeriesNavProps> = ({ series, active }) => {
const { t } = useTranslation();

if (series == null) {
return null;
}

const id = keyOfId(series.id);

const navEntries = [
{
url: `/~manage/series/${id}`,
page: "details",
body: <><LuPenLine />{t("manage.my-series.details.title")}</>,
},
];

if (CONFIG.allowAclEdit) {
navEntries.splice(1, 0, {
url: `/~manage/series/${id}/access`,
page: "acl",
body: <><LuShieldCheck />{t("manage.shared.acl.title")}</>,
});
}

const link = DirectSeriesRoute.url({ seriesId: id });
const title = series.title;
const ariaLabel = t("series.series-page", { series: series.title });

const additionalStyles = {
padding: 8,
borderBottom: `2px solid ${COLORS.neutral05}`,
};

const thumbnail = <>
<LuEye />
<SeriesThumbnail {...{ series }} />
</>;

return <ManageAssetNav {...{
active,
link,
ariaLabel,
title,
thumbnail,
navEntries,
additionalStyles,
}} />;
};
48 changes: 28 additions & 20 deletions frontend/src/routes/manage/Series/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import {
} from "./__generated__/SeriesManageQuery.graphql";
import { Link } from "../../../router";
import { ThumbnailStack } from "../../Search";
import { DirectSeriesRoute } from "../../Series";
import { SmallDescription } from "../../../ui/metadata";
import { keyOfId } from "../../../util";


const PATH = "/~manage/series" as const;
Expand Down Expand Up @@ -122,29 +122,12 @@ export const seriesColumns: ColumnProps[] = [

export const SeriesRow: React.FC<{ series: SingleSeries }> = ({ series }) => {
// Todo: change to "series details" route when available
const link = DirectSeriesRoute.url({ seriesId: series.id });

// Seems odd, but simply checking `e => e.__typename === "AuthorizedEvent"` will produce
// TS2339 errors when compiling.
type Entry = SingleSeries["entries"][number];
type AuthorizedEvent = Extract<Entry, { __typename: "AuthorizedEvent" }>;
const isAuthorizedEvent = (e: Entry): e is AuthorizedEvent =>
e.__typename === "AuthorizedEvent";

const thumbnails = series.entries
.filter(isAuthorizedEvent)
.map(e => ({
isLive: e.isLive,
audioOnly: e.syncedData ? e.syncedData.audioOnly : false,
thumbnail: e.syncedData?.thumbnail,
}));
const link = `${PATH}/${keyOfId(series.id)}`;

return (
<TableRow
thumbnail={<Link to={link} css={{ ...thumbnailLinkStyle }}>
<span css={{ "> div": { width: "100%" } }}>
<ThumbnailStack title={series.title} {...{ thumbnails }} />
</span>
<SeriesThumbnail {...{ series }} />
</Link>}
title={<Link to={link} css={{ ...titleLinkStyle }}>{series.title}</Link>}
description={series.syncedData && <SmallDescription
Expand Down Expand Up @@ -181,3 +164,28 @@ const queryParamsToSeriesVars = createQueryParamsParser<
offset,
})
);

type SeriesThumbnailProps = {
series: SingleSeries;
}

export const SeriesThumbnail: React.FC<SeriesThumbnailProps> = ({ series }) => {
// Seems odd, but simply checking `e => e.__typename === "AuthorizedEvent"` will produce
// TS2339 errors when compiling.
type Entry = SingleSeries["entries"][number];
type AuthorizedEvent = Extract<Entry, { __typename: "AuthorizedEvent" }>;
const isAuthorizedEvent = (e: Entry): e is AuthorizedEvent =>
e.__typename === "AuthorizedEvent";

const thumbnails = series.entries
.filter(isAuthorizedEvent)
.map(e => ({
isLive: e.isLive,
audioOnly: e.syncedData ? e.syncedData.audioOnly : false,
thumbnail: e.syncedData?.thumbnail,
}));

return <div css={{ "> div": { width: "100%" } }}>
<ThumbnailStack title={series.title} {...{ thumbnails }} />
</div>;
};
Loading

0 comments on commit 8c2b5a1

Please sign in to comment.