Skip to content

Commit

Permalink
Add name edit function to series, create series button in series w/o …
Browse files Browse the repository at this point in the history
…files util (#876)

* Add option to create series in series without files util

* Add name edit function to series

* Make mutation non-async in AddSeriesModal
  • Loading branch information
harshithmohan authored Apr 23, 2024
1 parent c928887 commit c16ca9c
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 148 deletions.
12 changes: 6 additions & 6 deletions src/components/Collection/Series/EditSeriesModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ type Props = {
};

const tabs = {
// name: 'Name',
// group: 'Group',
// stats: 'Personal Stats',
update_actions: 'Update Actions',
name: 'Name',
file_actions: 'File Actions',
delete_actions: 'Delete Actions',
};

const renderTab = (activeTab: string, seriesId: number) => {
switch (activeTab) {
case 'update_actions':
return <UpdateActionsTab seriesId={seriesId} />;
case 'name':
return <NameTab seriesId={seriesId} />;
case 'file_actions':
return <FileActionsTab seriesId={seriesId} />;
case 'delete_actions':
Expand All @@ -38,9 +38,9 @@ const renderTab = (activeTab: string, seriesId: number) => {
// return <GroupTab seriesId={seriesId} />;
// case 'stats':
// return <PersonalStats />;
case 'name':
case 'update_actions':
default:
return <NameTab seriesId={seriesId} />;
return <UpdateActionsTab seriesId={seriesId} />;
}
};

Expand Down Expand Up @@ -70,7 +70,7 @@ const EditSeriesModal = (props: Props) => {
</div>
</div>
<div className="border-r border-panel-border" />
<div>
<div className="grow">
{renderTab(activeTab, seriesId)}
</div>
</div>
Expand Down
174 changes: 78 additions & 96 deletions src/components/Collection/Series/EditSeriesTabs/NameTab.tsx
Original file line number Diff line number Diff line change
@@ -1,125 +1,107 @@
import React, { useEffect, useState } from 'react';
import { mdiCheckUnderlineCircleOutline, mdiCloseCircleOutline, mdiMagnify, mdiPencilCircleOutline } from '@mdi/js';
import cx from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import { mdiCheckUnderlineCircleOutline, mdiCloseCircleOutline, mdiLoading, mdiPencilCircleOutline } from '@mdi/js';
import { Icon } from '@mdi/react';
import { map } from 'lodash';
import { useToggle } from 'usehooks-ts';

import Input from '@/components/Input/Input';
import toast from '@/components/Toast';
import { useOverrideSeriesTitleMutation } from '@/core/react-query/series/mutations';
import { useSeriesQuery } from '@/core/react-query/series/queries';
// import { useLazyGetSeriesInfiniteQuery } from '@/core/rtkQuery/splitV3Api/seriesApi';

import type { SeriesTitleType } from '@/core/types/api/series';
import type { SeriesType } from '@/core/types/api/series';

type Props = {
seriesId: number;
};

const NameTab = ({ seriesId }: Props) => {
const [name, setName] = useState('');
const [search, setSearch] = useState('');
const [nameEditable, setNameEditable] = useState(false);
const [nameEditable, toggleNameEditable] = useToggle(false);

const seriesQuery = useSeriesQuery(seriesId, { includeDataFrom: ['AniDB'] });
const series = useMemo(() => seriesQuery?.data ?? {} as SeriesType, [seriesQuery.data]);

// TODO: Needs an actual endpoint to get series names instead of getting all series
// const [fetchSeries, seriesResults] = useLazyGetSeriesInfiniteQuery();
// const getAniDbSeries = useMemo((): SeriesTitleType[] => {
// const pages = seriesResults.data?.pages;
// if (!pages) return [];
//
// const keys = Object.keys(pages);
// if (!keys?.length) return [];
//
// return pages[1];
// }, [seriesResults]);

// const searchSeries = useMemo(() =>
// debounce(async () => {
// await fetchSeries({
// startsWith: search,
// pageSize: 5,
// });
// }, 250), [search, fetchSeries]);
const { mutate: overrideTitle } = useOverrideSeriesTitleMutation();

useEffect(() => {
setName(seriesQuery.data?.Name ?? '');
}, [seriesQuery.data?.Name]);

// useEffect(() => {
// if (!search) return;
// searchSeries()?.then()?.catch(console.error);
// }, [search, searchSeries]);

const renderTitle = (title: SeriesTitleType) => (
<div
className="flex cursor-pointer justify-between"
key={title.Language}
onClick={() => setName(title.Name)}
>
<div>{title.Name}</div>
{title.Language}
</div>
);
setName(series.Name ?? '');
}, [series.Name]);

const getNameInputIcons = () => {
const nameInputIcons = useMemo(() => {
if (!nameEditable) {
return [{
icon: mdiPencilCircleOutline,
className: 'text-panel-text-primary',
onClick: () => setNameEditable(_ => true),
onClick: toggleNameEditable,
}];
}

return [{
icon: mdiCloseCircleOutline,
className: 'text-red-500',
onClick: () => setName(_ => ''),
}, {
icon: mdiCheckUnderlineCircleOutline,
className: 'text-panel-text-primary',
onClick: () => {}, // TODO: Need endpoint to update series
}];
};
return [
{
icon: mdiCloseCircleOutline,
className: 'text-red-500',
onClick: toggleNameEditable,
},
{
icon: mdiCheckUnderlineCircleOutline,
className: 'text-panel-text-primary',
onClick: () =>
overrideTitle({ seriesId: series.IDs.ID, Title: name }, {
onSuccess: () => {
toast.success('Name updated successfully!');
toggleNameEditable();
},
onError: () => toast.error('Name could not be updated!'),
}),
},
];
}, [name, nameEditable, overrideTitle, series.IDs.ID, toggleNameEditable]);

return (
<div className="flex flex-col">
<Input
id="name"
type="text"
onChange={e => setName(e.target.value)}
value={name}
label="Name"
className="mb-4"
endIcons={getNameInputIcons()}
disabled={!nameEditable}
/>
<Input
id="search"
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
startIcon={mdiMagnify}
placeholder="Name Search..."
disabled={!nameEditable}
className={cx(!nameEditable && 'invisible')}
/>
<div
className={cx(
'mt-1 flex flex-col gap-y-2.5 rounded-lg border border-panel-border bg-panel-background-alt p-4 overflow-hidden',
!nameEditable && 'invisible',
)}
>
{!search && seriesQuery.data?.AniDB?.Titles.reduce((acc, title) => {
if (!search) {
acc.push(renderTitle(title));
return acc;
}
if (title.Name.toLowerCase().includes(search.toLowerCase())) {
acc.push(renderTitle(title));
return acc;
}
return acc;
}, [] as React.ReactNode[])}
{/* {search && getAniDbSeries.map(title => renderTitle(title))} */}
</div>
<div className="flex h-full flex-col">
{seriesQuery.isFetching && (
<div className="m-auto text-panel-text-primary">
<Icon path={mdiLoading} size={3} spin />
</div>
)}

{seriesQuery.isError && (
<div className="m-auto text-lg font-semibold text-panel-text-danger">
Series data could not be loaded!
</div>
)}

{seriesQuery.isSuccess && (
<>
<Input
id="name"
type="text"
onChange={e => setName(e.target.value)}
value={name}
label="Name"
className="mb-4"
endIcons={nameInputIcons}
disabled={!nameEditable}
/>
{nameEditable && (
<div className="flex overflow-y-auto rounded-lg border border-panel-border bg-panel-background-alt p-4">
<div className="flex grow flex-col gap-y-2.5 overflow-y-auto pr-4">
{map(series.AniDB?.Titles, title => (
<div
className="flex cursor-pointer justify-between"
key={title.Language}
onClick={() => setName(title.Name)}
>
<div>{title.Name}</div>
{title.Language}
</div>
))}
</div>
</div>
)}
</>
)}
</div>
);
};
Expand Down
130 changes: 130 additions & 0 deletions src/components/Utilities/SeriesWithoutFiles/AddSeriesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { mdiInformationOutline, mdiLoading, mdiMagnify, mdiOpenInNew } from '@mdi/js';
import { Icon } from '@mdi/react';
import cx from 'classnames';
import { filter } from 'lodash';
import { useDebounceValue } from 'usehooks-ts';

import Button from '@/components/Input/Button';
import Input from '@/components/Input/Input';
import ModalPanel from '@/components/Panels/ModalPanel';
import toast from '@/components/Toast';
import { invalidateQueries } from '@/core/react-query/queryClient';
import { useRefreshAniDBSeriesMutation } from '@/core/react-query/series/mutations';
import { useSeriesAniDBSearchQuery } from '@/core/react-query/series/queries';

type Props = {
show: boolean;
onClose: () => void;
};

const AddSeriesModal = ({ onClose, show }: Props) => {
const [searchText, setSearchText] = useState('');
const [debouncedSearch] = useDebounceValue(searchText, 200);

const searchQuery = useSeriesAniDBSearchQuery(debouncedSearch, !!debouncedSearch);

const {
isPending: isRefreshPending,
mutate: refreshSeries,
variables: refreshParams,
} = useRefreshAniDBSeriesMutation();

const createSeries = (anidbId: number) => {
refreshSeries({ anidbID: anidbId, immediate: true, createSeriesEntry: true }, {
onSuccess: () => {
toast.success('Series added successfully!');
invalidateQueries(['series', 'without-files']);
onClose();
},
onError: (error) => {
console.error(error);
toast.error('Failed to add series! Unable to create series entry.');
},
});
};

return (
<ModalPanel
show={show}
onRequestClose={onClose}
header="Add new series"
size="sm"
noPadding
>
<div className="flex flex-col gap-y-4 p-6">
<div className="flex justify-start gap-x-2">
<Icon className="shrink-0" path={mdiInformationOutline} size={1} />
<div className="flex">
Search for a series using the provided search, then click on a result to create an empty series (without
files) in Shoko.
</div>
</div>
<div className="flex flex-col gap-y-2">
<Input
id="search"
value={searchText}
type="text"
placeholder="Search..."
onChange={e => setSearchText(e.target.value)}
startIcon={mdiMagnify}
disabled={isRefreshPending}
/>
<div className="w-full rounded-lg border border-panel-border bg-panel-input p-4 capitalize">
<div className="flex h-60 flex-col gap-y-1 overflow-x-clip overflow-y-scroll rounded-lg bg-panel-input pr-2 ">
{searchQuery.isError || searchQuery.isFetching
? (
<div className="flex h-full items-center justify-center">
<Icon path={mdiLoading} size={3} spin className="text-panel-text-primary" />
</div>
)
: filter(searchQuery.data, result => !result.ShokoID)
.map(result => (
<div
key={result.ID}
className={cx(
'flex cursor-pointer items-center justify-between',
isRefreshPending && 'pointer-events-none',
isRefreshPending && result.ID !== refreshParams?.anidbID && 'opacity-50',
)}
onClick={() => createSeries(result.ID)}
>
<div className="line-clamp-1">{result.Title}</div>
{result.ID === refreshParams?.anidbID
? (
<div className="text-panel-text-primary">
<Icon path={mdiLoading} size={0.833} spin />
</div>
)
: (
<a
href={`https://anidb.net/anime/${result.ID}`}
target="_blank"
rel="noopener noreferrer"
className="text-panel-text-primary"
aria-label="Open AniDB series page"
>
<Icon path={mdiOpenInNew} size={0.833} />
</a>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex justify-end">
<Button
buttonType="secondary"
buttonSize="normal"
className="flex items-center justify-center"
onClick={onClose}
>
Cancel
</Button>
</div>
</div>
</ModalPanel>
);
};

export default AddSeriesModal;
9 changes: 9 additions & 0 deletions src/core/react-query/series/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ export const useGetSeriesAniDBMutation = () =>
mutationFn: (anidbId: number) => axios.get(`Series/AniDB/${anidbId}`),
});

export const useOverrideSeriesTitleMutation = () =>
useMutation({
mutationFn: ({ seriesId, ...data }: { seriesId: number, Title: string }) =>
axios.post(`Series/${seriesId}/OverrideTitle`, data),
onSuccess: () => {
invalidateQueries(['series', 'single']);
},
});

export const useRefreshAniDBSeriesMutation = () =>
useMutation<boolean, unknown, RefreshAniDBSeriesRequestType>({
mutationFn: ({ anidbID, ...params }: RefreshAniDBSeriesRequestType) =>
Expand Down
Loading

0 comments on commit c16ca9c

Please sign in to comment.