Skip to content

Commit

Permalink
Merge pull request #10 from fearnlj01/series-redesign
Browse files Browse the repository at this point in the history
Tag filtering within series pages, Group management in Edit Series Modal
  • Loading branch information
ElementalCrisis authored May 26, 2024
2 parents 838b0a1 + 221013c commit cb5f978
Show file tree
Hide file tree
Showing 25 changed files with 947 additions and 545 deletions.
10 changes: 9 additions & 1 deletion src/components/Collection/CollectionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Props = {
items: CollectionGroupType[] | SeriesType[];
mode: string;
total: number;
setEditSeriesModalId: (seriesId: number) => void;
};

const CollectionView = (props: Props) => {
Expand All @@ -37,6 +38,7 @@ const CollectionView = (props: Props) => {
isSidebarOpen,
items,
mode,
setEditSeriesModalId,
total,
} = props;

Expand Down Expand Up @@ -146,7 +148,12 @@ const CollectionView = (props: Props) => {
);
} else if (mode === 'poster') {
children.push(
<PosterViewItem item={item} key={`group-${item.IDs.ID}`} isSeries={isSeries} />,
<PosterViewItem
key={`group-${item.IDs.ID}`}
item={item}
isSeries={isSeries}
setEditSeriesModalId={setEditSeriesModalId}
/>,
);
} else {
children.push(
Expand All @@ -158,6 +165,7 @@ const CollectionView = (props: Props) => {
key={`group-${item.IDs.ID}`}
isSeries={isSeries}
isSidebarOpen={isSidebarOpen}
setEditSeriesModalId={setEditSeriesModalId}
/>,
);
}
Expand Down
113 changes: 113 additions & 0 deletions src/components/Collection/Credits/CreditsSearchAndFilterPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, {} from 'react';
import { mdiInformationOutline, mdiMagnify, mdiPlayCircleOutline } from '@mdi/js';
import Icon from '@mdi/react';

import Checkbox from '@/components/Input/Checkbox';
import Input from '@/components/Input/Input';
import ShokoPanel from '@/components/Panels/ShokoPanel';

type Props = {
inputPlaceholder: string;
search: string;
handleSearchChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
roleFilter: Set<string>;
handleFilterChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
uniqueRoles: string[];
refreshAniDbAction: () => void;
aniDbRefreshing: boolean;
refreshTvDbAction: () => void;
tvDbRefreshing: boolean;
};
const CreditsSearchAndFilterPanel = React.memo(
(
{
aniDbRefreshing,
handleFilterChange,
handleSearchChange,
inputPlaceholder,
refreshAniDbAction,
refreshTvDbAction,
roleFilter,
search,
tvDbRefreshing,
uniqueRoles,
}: Props,
) => (
<ShokoPanel
title="Search & Filter"
className="w-400"
contentClassName="gap-y-6"
sticky
transparent
fullHeight={false}
>
<div className="flex flex-col gap-y-2">
<span className="flex w-full text-base font-semibold">
Name
</span>
<Input
id="search"
startIcon={mdiMagnify}
type="text"
placeholder={inputPlaceholder}
value={search}
inputClassName="px-4 py-3"
onChange={handleSearchChange}
/>
</div>
<div className="flex flex-col gap-y-2">
<div className="text-base font-semibold">Roles</div>
<div className="flex flex-col gap-y-2 rounded-lg bg-panel-input p-6">
{uniqueRoles.map(desc => (
<Checkbox
justify
label={desc}
key={desc}
id={desc}
isChecked={!roleFilter.has(desc)}
onChange={handleFilterChange}
/>
))}
</div>
</div>
<div className="flex flex-col gap-y-2">
<div className="text-base font-semibold">Quick Actions</div>
<button
type="button"
className="flex w-full flex-row justify-between disabled:cursor-not-allowed disabled:opacity-65"
onClick={refreshAniDbAction}
disabled={aniDbRefreshing}
>
Force refresh: AniDB
<Icon
path={mdiPlayCircleOutline}
className="pointer-events-auto text-panel-icon-action group-disabled:cursor-not-allowed"
size={1}
/>
</button>
<button
type="button"
className="flex w-full flex-row justify-between disabled:cursor-not-allowed disabled:opacity-65"
onClick={refreshTvDbAction}
disabled={tvDbRefreshing}
>
Force refresh: TVDB
<Icon
path={mdiPlayCircleOutline}
className="pointer-events-auto text-panel-icon-action group-disabled:cursor-not-allowed"
size={1}
/>
</button>
</div>
<hr className="border border-panel-border" />
<div className="flex flex-row gap-x-3">
<Icon path={mdiInformationOutline} className="text-panel-icon-warning" size={1} />
<div className="grow text-base font-semibold">
Warning! Possible Spoilers
</div>
</div>
</ShokoPanel>
),
);

export default CreditsSearchAndFilterPanel;
38 changes: 38 additions & 0 deletions src/components/Collection/Credits/CreditsStaffPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import CharacterImage from '@/components/CharacterImage';

import type { SeriesCast } from '@/core/types/api/series';
import type { CreditsModeType } from '@/pages/collection/series/SeriesCredits';

const getThumbnailUrl = (item: SeriesCast, mode: CreditsModeType) => {
const thumbnail = item[mode].Image ?? null;
if (thumbnail === null) return null;
return `/api/v3/Image/${thumbnail.Source}/${thumbnail.Type}/${thumbnail.ID}`;
};

const CreditsStaffPanel = React.memo(({ cast, mode }: { cast: SeriesCast, mode: CreditsModeType }) => (
<div className="flex w-full flex-row items-center gap-6 rounded-lg border border-panel-border bg-panel-background-transparent p-6 font-semibold">
<div className="z-10 flex gap-x-2">
{mode === 'Character' && (
<CharacterImage
imageSrc={getThumbnailUrl(cast, 'Character')}
className="relative h-[7.75rem] w-[6.063rem] rounded-lg"
/>
)}
<CharacterImage
imageSrc={getThumbnailUrl(cast, 'Staff')}
className="relative h-[7.75rem] w-[6.063rem] rounded-lg"
/>
</div>
<div className="grow text-center">
<div className="line-clamp-2 text-base leading-8 xl:text-xl" title={cast[mode]?.Name}>
{cast[mode]?.Name}
</div>
{mode === 'Character' && <div className="opacity-65">{cast.Staff?.Name}</div>}
<div className="mt-2 text-sm">{cast.RoleDetails}</div>
</div>
</div>
));

export default CreditsStaffPanel;
43 changes: 43 additions & 0 deletions src/components/Collection/Credits/CreditsStaffVirtualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import { useOutletContext } from 'react-router';
import { useVirtualizer } from '@tanstack/react-virtual';

import CreditsStaffPanel from '@/components/Collection/Credits/CreditsStaffPanel';

import type { SeriesCast } from '@/core/types/api/series';
import type { CreditsModeType } from '@/pages/collection/series/SeriesCredits';

const StaffPanelVirtualizer = ({ castArray, mode }: { castArray: SeriesCast[], mode: CreditsModeType }) => {
const { scrollRef } = useOutletContext<{ scrollRef: React.RefObject<HTMLDivElement> }>();
const cardSize = { x: 466.5, y: 174, gap: 24 };

const rowVirtualizer = useVirtualizer({
count: castArray.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => cardSize.y,
overscan: 30, // Greater than the norm as lanes aren't taken into account
lanes: 3,
gap: cardSize.gap,
});

return (
<div className="relative w-full" style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.key}
className="absolute top-0"
style={{
left: virtualRow.lane * (cardSize.x + cardSize.gap),
width: cardSize.x,
height: cardSize.y,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<CreditsStaffPanel cast={castArray[virtualRow.index]} mode={mode} />
</div>
))}
</div>
);
};

export default StaffPanelVirtualizer;
2 changes: 1 addition & 1 deletion src/components/Collection/Episode/EpisodeSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const EpisodeSummary = React.memo(
icon={mdiEyeOffOutline}
active={episode.IsHidden}
onClick={handleMarkHidden}
tooltip={`${episode.IsHidden ? 'Hide' : 'Unhide'} Episode`}
tooltip={`${episode.IsHidden ? 'Unhide' : 'Hide'} Episode`}
/>
</div>
</div>
Expand Down
29 changes: 21 additions & 8 deletions src/components/Collection/ListViewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { listItemSize } from '@/components/Collection/constants';
import { useSeriesTagsQuery } from '@/core/react-query/series/queries';
import { useSettingsQuery } from '@/core/react-query/settings/queries';
import { dayjs, formatThousand } from '@/core/util';
import useEventCallback from '@/hooks/useEventCallback';
import useMainPoster from '@/hooks/useMainPoster';

import AnidbDescription from './AnidbDescription';
Expand Down Expand Up @@ -58,9 +59,10 @@ type Props = {
isSeries?: boolean;
groupExtras?: WebuiGroupExtra;
isSidebarOpen: boolean;
setEditSeriesModalId: (seriesId: number) => void;
};

const ListViewItem = ({ groupExtras, isSeries, isSidebarOpen, item }: Props) => {
const ListViewItem = ({ groupExtras, isSeries, isSidebarOpen, item, setEditSeriesModalId }: Props) => {
const settings = useSettingsQuery().data;
const { showCustomTags, showGroupIndicator, showItemType, showTopTags } = settings.WebUI_Settings.collection.list;

Expand Down Expand Up @@ -120,6 +122,12 @@ const ListViewItem = ({ groupExtras, isSeries, isSidebarOpen, item }: Props) =>
[isSeries, groupExtras?.Tags, tagsQuery.data, showCustomTags, showTopTags],
);

const editSeriesModalCallback = useEventCallback((event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
setEditSeriesModalId(('MainSeries' in item.IDs) ? item.IDs.MainSeries : item.IDs.ID);
});

return (
<div
className="flex h-full shrink-0 grow flex-col content-center gap-y-3 rounded-lg border border-panel-border bg-panel-background p-6"
Expand All @@ -136,13 +144,18 @@ const ListViewItem = ({ groupExtras, isSeries, isSidebarOpen, item }: Props) =>
zoomOnHover
>
<div className="pointer-events-none z-10 flex h-full bg-panel-background-poster-overlay p-3 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
<Link to="#" className="h-fit">
<Icon
path={mdiPencilCircleOutline}
size="2rem"
className="text-panel-icon"
/>
</Link>
{(isSeries || item.Size === 1) && (
<div
className="pointer-events-auto h-fit"
onClick={editSeriesModalCallback}
>
<Icon
path={mdiPencilCircleOutline}
size="2rem"
className="text-panel-icon"
/>
</div>
)}
</div>
{showGroupIndicator && groupCount > 1 && (
<div className="absolute bottom-0 left-0 flex w-full justify-center rounded-bl-md bg-panel-background-overlay py-1.5 text-sm font-semibold opacity-100 transition-opacity group-hover:opacity-0">
Expand Down
32 changes: 22 additions & 10 deletions src/components/Collection/PosterViewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { reduce } from 'lodash';

import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlaceholderDiv';
import { useSettingsQuery } from '@/core/react-query/settings/queries';
import useEventCallback from '@/hooks/useEventCallback';
import useMainPoster from '@/hooks/useMainPoster';

import type { CollectionGroupType } from '@/core/types/api/collection';
Expand All @@ -14,9 +15,10 @@ import type { SeriesType } from '@/core/types/api/series';
type Props = {
item: CollectionGroupType | SeriesType;
isSeries?: boolean;
setEditSeriesModalId: (seriesId: number) => void;
};

const PosterViewItem = ({ isSeries = false, item }: Props) => {
const PosterViewItem = ({ isSeries = false, item, setEditSeriesModalId }: Props) => {
const settings = useSettingsQuery().data;
const { showEpisodeCount, showGroupIndicator, showUnwatchedCount } = settings.WebUI_Settings.collection.poster;

Expand All @@ -43,6 +45,12 @@ const PosterViewItem = ({ isSeries = false, item }: Props) => {
return link;
};

const editSeriesModalCallback = useEventCallback((event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
setEditSeriesModalId(('MainSeries' in item.IDs) ? item.IDs.MainSeries : item.IDs.ID);
});

return (
<Link to={viewRouteLink()}>
<div
Expand All @@ -62,15 +70,19 @@ const PosterViewItem = ({ isSeries = false, item }: Props) => {
)}
</div>
)}
<div className="pointer-events-none z-50 flex h-full bg-panel-background-poster-overlay p-3 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
{/* FIXME: This can't be a <Link> otherwise Warning: validateDOMNesting(...): <a> cannot appear as a descendant of <a> happens, BackgroundImagePlaceholderDiv wraps everything in a <Link> internally */}
<span className="h-fit">
<Icon
path={mdiPencilCircleOutline}
size="2rem"
className="text-panel-icon"
/>
</span>
<div className="pointer-events-none z-10 flex h-full bg-panel-background-poster-overlay p-3 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
{(isSeries || item.Size === 1) && (
<div
className="pointer-events-auto h-fit"
onClick={editSeriesModalCallback}
>
<Icon
path={mdiPencilCircleOutline}
size="2rem"
className="text-panel-icon"
/>
</div>
)}
</div>
{showGroupIndicator && !isSeries && groupCount > 1 && (
<div className="absolute bottom-4 left-3 flex w-[90%] justify-center rounded-lg bg-panel-background-overlay py-2 text-sm font-semibold text-panel-text opacity-100 transition-opacity group-hover:opacity-0">
Expand Down
Loading

0 comments on commit cb5f978

Please sign in to comment.