Files
Homarr/src/widgets/media-requests/MediaRequestListTile.tsx

254 lines
7.4 KiB
TypeScript
Raw Normal View History

import {
ActionIcon,
Badge,
Card,
Center,
Flex,
Group,
Image,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
2023-07-21 18:08:40 +09:00
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
2023-07-21 18:08:40 +09:00
2023-04-04 22:32:08 +02:00
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useMediaRequestQuery } from './media-request-query';
import { MediaRequest, MediaRequestStatus } from './media-request-types';
2023-04-04 22:32:08 +02:00
const definition = defineWidget({
id: 'media-requests-list',
icon: IconGitPullRequest,
options: {
replaceLinksWithExternalHost: {
type: 'switch',
defaultValue: true,
},
},
2023-04-04 22:32:08 +02:00
component: MediaRequestListTile,
gridstack: {
minWidth: 3,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
});
export type MediaRequestListWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface MediaRequestListWidgetProps {
widget: MediaRequestListWidget;
}
type MediaRequestDecisionVariables = {
request: MediaRequest;
isApproved: boolean;
};
const useMediaRequestDecisionMutation = () => {
const { name: configName } = useConfigContext();
const utils = api.useContext();
const { mutateAsync } = api.overseerr.decide.useMutation({
onSuccess() {
utils.mediaRequest.all.invalidate();
},
});
return async (variables: MediaRequestDecisionVariables) => {
const action = variables.isApproved ? 'Approving' : 'Declining';
notifications.show({
id: `decide-${variables.request.id}`,
color: 'yellow',
title: `${action} request...`,
message: undefined,
loading: true,
});
await mutateAsync(
{
configName: configName!,
id: variables.request.id,
isApproved: variables.isApproved,
},
{
onSuccess(_data, variables) {
const title = variables.isApproved ? 'Request was approved!' : 'Request was declined!';
notifications.update({
id: `decide-${variables.id}`,
color: 'teal',
title,
message: undefined,
icon: <IconCheck size="1rem" />,
autoClose: 2000,
});
},
}
);
};
};
2023-04-04 22:32:08 +02:00
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
const { t } = useTranslation('modules/media-requests-list');
const { data, isLoading } = useMediaRequestQuery();
// Use mutation to approve or deny a pending request
const decideAsync = useMediaRequestDecisionMutation();
2023-04-04 22:32:08 +02:00
if (!data || isLoading) {
2023-04-04 22:32:08 +02:00
return <WidgetLoading />;
}
if (data.length === 0) {
return (
<Center h="100%">
<Text>{t('noRequests')}</Text>
2023-04-04 22:32:08 +02:00
</Center>
);
}
const countPendingApproval = data.filter(
(x) => x.status === MediaRequestStatus.PendingApproval
).length;
// Return a sorted data by status to show pending first, then the default order
const sortedData = data.sort((a: MediaRequest, b: MediaRequest) => {
if (a.status === MediaRequestStatus.PendingApproval) {
return -1;
}
if (b.status === MediaRequestStatus.PendingApproval) {
return 1;
}
return 0;
});
2023-04-04 22:32:08 +02:00
return (
<Stack>
{countPendingApproval > 0 ? (
<Text>{t('pending', { countPendingApproval })}</Text>
2023-04-04 22:32:08 +02:00
) : (
<Text>{t('nonePending')}</Text>
2023-04-04 22:32:08 +02:00
)}
{sortedData.map((item) => (
2023-04-04 22:32:08 +02:00
<Card pos="relative" withBorder>
<Flex justify="space-between" gap="md">
<Flex gap="md">
<Image
src={item.posterPath}
width={30}
height={50}
alt="poster"
radius="xs"
withPlaceholder
/>
<Stack spacing={0}>
<Group spacing="xs">
{item.airDate && <Text>{item.airDate.split('-')[0]}</Text>}
2023-04-04 22:32:08 +02:00
<MediaRequestStatusBadge status={item.status} />
</Group>
<Text
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
lineClamp={1}
weight="bold"
component="a"
href={item.href}
>
{item.name}
</Text>
</Stack>
</Flex>
<Stack justify="center">
<Flex gap="xs">
<Image
src={item.userProfilePicture}
width={25}
height={25}
alt="requester avatar"
radius="xl"
withPlaceholder
/>
<Text
component="a"
href={item.userLink}
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
>
{item.userName}
</Text>
</Flex>
{item.status === MediaRequestStatus.PendingApproval && (
<Group>
<Tooltip label={t('tooltips.approve')} withArrow withinPortal>
<ActionIcon
variant="light"
color="green"
onClick={async () => {
notifications.show({
id: `approve ${item.id}`,
color: 'yellow',
title: 'Approving request...',
message: undefined,
loading: true,
});
await decideAsync({
request: item,
isApproved: true,
});
}}
>
<IconThumbUp />
</ActionIcon>
</Tooltip>
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
<ActionIcon
variant="light"
color="red"
onClick={async () => {
await decideAsync({
request: item,
isApproved: false,
});
}}
>
<IconThumbDown />
</ActionIcon>
</Tooltip>
</Group>
)}
</Stack>
2023-04-04 22:32:08 +02:00
</Flex>
<Image
src={item.backdropPath}
pos="absolute"
w="100%"
h="100%"
opacity={0.1}
top={0}
left={0}
style={{ pointerEvents: 'none' }}
/>
</Card>
))}
</Stack>
);
}
const MediaRequestStatusBadge = ({ status }: { status: MediaRequestStatus }) => {
const { t } = useTranslation('modules/media-requests-list');
2023-04-04 22:32:08 +02:00
switch (status) {
case MediaRequestStatus.Approved:
return <Badge color="green">{t('state.approved')}</Badge>;
2023-04-04 22:32:08 +02:00
case MediaRequestStatus.Declined:
return <Badge color="red">{t('state.declined')}</Badge>;
2023-04-04 22:32:08 +02:00
case MediaRequestStatus.PendingApproval:
return <Badge color="orange">{t('state.pendingApproval')}</Badge>;
2023-04-04 22:32:08 +02:00
default:
return <></>;
}
};
export default definition;