Skip to content

Commit

Permalink
feat: add refresh metadata button to cards
Browse files Browse the repository at this point in the history
  • Loading branch information
oae committed Jan 7, 2023
1 parent f75c878 commit cba8a74
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 105 deletions.
156 changes: 52 additions & 104 deletions src/components/mangaCard.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import {
ActionIcon,
Alert,
Badge,
Box,
Button,
Checkbox,
Code,
createStyles,
Paper,
Skeleton,
Text,
Title,
} from '@mantine/core';
import { useModals } from '@mantine/modals';
import { ActionIcon, Badge, createStyles, Paper, Skeleton, Title, Tooltip } from '@mantine/core';
import { Prisma } from '@prisma/client';
import { IconEdit, IconX } from '@tabler/icons';
import { IconEdit, IconRefresh, IconX } from '@tabler/icons';
import { contrastColor } from 'contrast-color';
import { useState } from 'react';
import stc from 'string-to-color';
import { useRefreshModal } from './refreshMetadata';
import { useRemoveModal } from './removeManga';
import { useUpdateModal } from './updateManga';

const useStyles = createStyles((theme, _params, getRef) => ({
Expand Down Expand Up @@ -49,6 +36,9 @@ const useStyles = createStyles((theme, _params, getRef) => ({
[`&:hover .${getRef('editButton')}`]: {
display: 'flex',
},
[`&:hover .${getRef('refreshButton')}`]: {
display: 'flex',
},
},
removeButton: {
ref: getRef('removeButton'),
Expand All @@ -57,6 +47,18 @@ const useStyles = createStyles((theme, _params, getRef) => ({
top: -5,
display: 'none',
},
refreshButton: {
ref: getRef('refreshButton'),
backgroundColor: theme.white,
color: theme.colors.blue[6],
position: 'absolute',
right: 10,
bottom: 50,
display: 'none',
'&:hover': {
backgroundColor: theme.colors.gray[0],
},
},
editButton: {
ref: getRef('editButton'),
backgroundColor: theme.white,
Expand Down Expand Up @@ -93,92 +95,21 @@ interface MangaCardProps {
manga: MangaWithLibraryAndMetadata;
onRemove: (shouldRemoveFiles: boolean) => void;
onUpdate: () => void;
onRefresh: () => void;
onClick: () => void;
}

function RemoveModalContent({
title,
onRemove,
onClose,
}: {
title: string;
onRemove: (shouldRemoveFiles: boolean) => void;
onClose: () => void;
}) {
const [shouldRemoveFiles, setShouldRemoveFiles] = useState(false);
return (
<>
<Text mb={4} size="sm">
Are you sure you want to remove
<Code className="text-base font-bold" color="red">
{title}
</Code>
?
</Text>
<Alert
icon={
<Checkbox
checked={shouldRemoveFiles}
color="red"
onChange={(event) => setShouldRemoveFiles(event.currentTarget.checked)}
/>
}
title="Remove files?"
color="red"
>
This action is destructive and all downloaded files will be removed
</Alert>
<Box
sx={(theme) => ({
display: 'flex',
gap: theme.spacing.xs,
justifyContent: 'end',
marginTop: theme.spacing.md,
})}
>
<Button variant="default" color="dark" onClick={onClose}>
Cancel
</Button>
<Button
variant="filled"
color="red"
onClick={() => {
onRemove(shouldRemoveFiles);
onClose();
}}
>
Remove
</Button>
</Box>
</>
);
}

const useRemoveModal = (title: string, onRemove: (shouldRemoveFiles: boolean) => void) => {
const modals = useModals();

const openRemoveModal = () => {
const id = modals.openModal({
title: `Remove ${title}?`,
centered: true,
children: <RemoveModalContent title={title} onRemove={onRemove} onClose={() => modals.closeModal(id)} />,
});
};

return openRemoveModal;
};

export function SkeletonMangaCard() {
const { classes } = useStyles();

return <Skeleton radius="md" className={classes.skeletonCard} />;
}

export function MangaCard({ manga, onRemove, onUpdate, onClick }: MangaCardProps) {
export function MangaCard({ manga, onRemove, onUpdate, onRefresh, onClick }: MangaCardProps) {
const { classes } = useStyles();
const removeModal = useRemoveModal(manga.title, onRemove);
const refreshModal = useRefreshModal(manga.title, onRefresh);
const updateModal = useUpdateModal(manga, onUpdate);

return (
<Paper
onClick={onClick}
Expand All @@ -202,19 +133,36 @@ export function MangaCard({ manga, onRemove, onUpdate, onClick }: MangaCardProps
>
<IconX size={16} />
</ActionIcon>
<ActionIcon
color="blue"
variant="light"
size="lg"
radius="xl"
className={classes.editButton}
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
updateModal();
}}
>
<IconEdit size={18} />
</ActionIcon>
<Tooltip withinPortal withArrow label="Refresh Metadata" position="left">
<ActionIcon
color="teal"
variant="filled"
size="lg"
radius="xl"
className={classes.refreshButton}
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
refreshModal();
}}
>
<IconRefresh size={18} />
</ActionIcon>
</Tooltip>
<Tooltip withinPortal withArrow label="Edit" position="left">
<ActionIcon
color="blue"
variant="light"
size="lg"
radius="xl"
className={classes.editButton}
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
updateModal();
}}
>
<IconEdit size={18} />
</ActionIcon>
</Tooltip>
<div>
<Badge
sx={{ backgroundColor: stc(manga.source), color: contrastColor({ bgColor: stc(manga.source) }) }}
Expand Down
48 changes: 48 additions & 0 deletions src/components/refreshMetadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box, Button, Text } from '@mantine/core';
import { useModals } from '@mantine/modals';

function RefreshMetadataModalContent({ onRefresh, onClose }: { onRefresh: () => void; onClose: () => void }) {
return (
<>
<Text mb={4} size="sm">
This will update all downloaded chapters with the latest metadata from AniList
</Text>
<Box
sx={(theme) => ({
display: 'flex',
gap: theme.spacing.xs,
justifyContent: 'end',
marginTop: theme.spacing.md,
})}
>
<Button variant="default" color="dark" onClick={onClose}>
Cancel
</Button>
<Button
variant="filled"
color="teal"
onClick={() => {
onRefresh();
onClose();
}}
>
Refresh
</Button>
</Box>
</>
);
}

export const useRefreshModal = (title: string, onRefresh: () => void) => {
const modals = useModals();

const openRemoveModal = () => {
const id = modals.openModal({
title: `Refresh Metadata for ${title}?`,
centered: true,
children: <RefreshMetadataModalContent onRefresh={onRefresh} onClose={() => modals.closeModal(id)} />,
});
};

return openRemoveModal;
};
75 changes: 75 additions & 0 deletions src/components/removeManga.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Alert, Box, Button, Checkbox, Code, Text } from '@mantine/core';
import { useModals } from '@mantine/modals';
import { useState } from 'react';

function RemoveModalContent({
title,
onRemove,
onClose,
}: {
title: string;
onRemove: (shouldRemoveFiles: boolean) => void;
onClose: () => void;
}) {
const [shouldRemoveFiles, setShouldRemoveFiles] = useState(false);
return (
<>
<Text mb={4} size="sm">
Are you sure you want to remove
<Code className="text-base font-bold" color="red">
{title}
</Code>
?
</Text>
<Alert
icon={
<Checkbox
checked={shouldRemoveFiles}
color="red"
onChange={(event) => setShouldRemoveFiles(event.currentTarget.checked)}
/>
}
title="Remove files?"
color="red"
>
This action is destructive and all downloaded files will be removed
</Alert>
<Box
sx={(theme) => ({
display: 'flex',
gap: theme.spacing.xs,
justifyContent: 'end',
marginTop: theme.spacing.md,
})}
>
<Button variant="default" color="dark" onClick={onClose}>
Cancel
</Button>
<Button
variant="filled"
color="red"
onClick={() => {
onRemove(shouldRemoveFiles);
onClose();
}}
>
Remove
</Button>
</Box>
</>
);
}

export const useRemoveModal = (title: string, onRemove: (shouldRemoveFiles: boolean) => void) => {
const modals = useModals();

const openRemoveModal = () => {
const id = modals.openModal({
title: `Remove ${title}?`,
centered: true,
children: <RemoveModalContent title={title} onRemove={onRemove} onClose={() => modals.closeModal(id)} />,
});
};

return openRemoveModal;
};
34 changes: 34 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { trpc } from '../utils/trpc';
export default function IndexPage() {
const libraryQuery = trpc.library.query.useQuery();
const mangaRemove = trpc.manga.remove.useMutation();
const mangaRefresh = trpc.manga.refreshMetaData.useMutation();
const router = useRouter();

const mangaQuery = trpc.manga.query.useQuery();
Expand Down Expand Up @@ -79,6 +80,38 @@ export default function IndexPage() {
mangaQuery.refetch();
};

const handleRefresh = async (id: number, title: string) => {
try {
await mangaRefresh.mutateAsync({
id,
});
showNotification({
icon: <IconCheck size={18} />,
color: 'teal',
autoClose: true,
title: 'Manga',
message: (
<Text>
<Code color="blue">{title}</Code> chapters are queued for the metadata update
</Text>
),
});
} catch (err) {
showNotification({
icon: <IconX size={18} />,
color: 'red',
autoClose: true,
title: 'Manga',
message: (
<Text>
<Code color="red">{`${err}`}</Code>
</Text>
),
});
}
mangaQuery.refetch();
};

return (
<ScrollArea sx={{ height: 'calc(100vh - 88px)' }}>
<Grid m={12} justify="flex-start">
Expand All @@ -91,6 +124,7 @@ export default function IndexPage() {
<Grid.Col span="content" key={manga.id}>
<MangaCard
manga={manga}
onRefresh={() => handleRefresh(manga.id, manga.title)}
onUpdate={() => mangaQuery.refetch()}
onRemove={(shouldRemoveFiles: boolean) => handleRemove(manga.id, manga.title, shouldRemoveFiles)}
onClick={() => router.push(`/manga/${manga.id}`)}
Expand Down

0 comments on commit cba8a74

Please sign in to comment.