feat(requests): add request quotas (#1277)

* feat(quotas): rebased

* feat: add getQuota() method to User entity

* feat(ui): add default quota setting options

* feat: user quota settings

* feat: quota display in request modals

* fix: only show user quotas on own profile or with manage users permission

* feat: add request progress circles to profile page

* feat: add migration

* fix: add missing restricted field to api schema

* fix: dont show auto approve message for movie request when restricted

* fix(lang): change enable checkbox langauge to "enable override"

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
This commit is contained in:
sct
2021-03-24 19:26:13 +09:00
committed by GitHub
parent a65e3d5bb6
commit 6c75c88228
24 changed files with 1212 additions and 145 deletions

View File

@@ -1,22 +1,33 @@
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { useUser } from '../../hooks/useUser';
import Error from '../../pages/_error';
import LoadingSpinner from '../Common/LoadingSpinner';
import { UserRequestsResponse } from '../../../server/interfaces/api/userInterfaces';
import Slider from '../Slider';
import RequestCard from '../RequestCard';
import {
QuotaResponse,
UserRequestsResponse,
} from '../../../server/interfaces/api/userInterfaces';
import { MovieDetails } from '../../../server/models/Movie';
import { TvDetails } from '../../../server/models/Tv';
import { Permission, useUser } from '../../hooks/useUser';
import Error from '../../pages/_error';
import ImageFader from '../Common/ImageFader';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import ProgressCircle from '../Common/ProgressCircle';
import RequestCard from '../RequestCard';
import Slider from '../Slider';
import ProfileHeader from './ProfileHeader';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
recentrequests: 'Recent Requests',
norequests: 'No Requests',
limit: '{remaining} of {limit}',
requestsperdays: '{limit} remaining',
unlimited: 'Unlimited',
totalrequests: 'Total Requests',
pastdays: '{type} (past {days} days)',
movierequests: 'Movie Requests',
seriesrequest: 'Series Requests',
});
type MediaTitle = MovieDetails | TvDetails;
@@ -27,6 +38,7 @@ const UserProfile: React.FC = () => {
const { user, error } = useUser({
id: Number(router.query.userId),
});
const { user: currentUser, hasPermission: currentHasPermission } = useUser();
const [availableTitles, setAvailableTitles] = useState<
Record<number, MediaTitle>
>({});
@@ -34,6 +46,9 @@ const UserProfile: React.FC = () => {
const { data: requests, error: requestError } = useSWR<UserRequestsResponse>(
user ? `/api/v1/user/${user?.id}/requests?take=10&skip=0` : null
);
const { data: quota } = useSWR<QuotaResponse>(
user ? `/api/v1/user/${user.id}/quota` : null
);
const updateAvailableTitles = useCallback(
(requestId: number, mediaTitle: MediaTitle) => {
@@ -76,6 +91,140 @@ const UserProfile: React.FC = () => {
</div>
)}
<ProfileHeader user={user} />
{quota &&
(user.id === currentUser?.id ||
currentHasPermission(Permission.MANAGE_USERS)) && (
<div className="relative z-40">
<dl className="grid grid-cols-1 gap-5 mt-5 lg:grid-cols-3">
<div className="px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ring-gray-700 sm:p-6">
<dt className="text-sm font-medium text-gray-300 truncate">
{intl.formatMessage(messages.totalrequests)}
</dt>
<dd className="mt-1 text-3xl font-semibold text-white">
{intl.formatNumber(user.requestCount)}
</dd>
</div>
<div
className={`px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ${
quota.movie.restricted
? 'ring-red-500 from-red-900 to-transparent bg-gradient-to-t'
: 'ring-gray-700'
} sm:p-6`}
>
<dt
className={`text-sm font-medium truncate ${
quota.movie.restricted ? 'text-red-500' : 'text-gray-300'
}`}
>
{quota.tv.limit
? intl.formatMessage(messages.pastdays, {
type: intl.formatMessage(messages.movierequests),
days: quota?.movie.days,
})
: intl.formatMessage(messages.movierequests)}
</dt>
<dd
className={`flex mt-1 text-sm items-center ${
quota.movie.restricted ? 'text-red-500' : 'text-white'
}`}
>
{quota.movie.limit ? (
<>
<ProgressCircle
progress={Math.max(
0,
Math.round(
((quota?.movie.remaining ?? 0) /
(quota?.movie.limit ?? 1)) *
100
)
)}
useHeatLevel
className="w-8 h-8 mr-2"
/>
<div>
{intl.formatMessage(messages.requestsperdays, {
limit: (
<span className="text-3xl font-semibold">
{intl.formatMessage(messages.limit, {
remaining: quota.movie.remaining,
limit: quota.movie.limit,
})}
</span>
),
})}
</div>
</>
) : (
<span className="text-3xl">
{intl.formatMessage(messages.unlimited)}
</span>
)}
</dd>
</div>
<div
className={`px-4 py-5 overflow-hidden bg-gray-800 bg-opacity-50 rounded-lg shadow ring-1 ${
quota.tv.restricted
? 'ring-red-500 from-red-900 to-transparent bg-gradient-to-t'
: 'ring-gray-700'
} sm:p-6`}
>
<dt
className={`text-sm font-medium truncate ${
quota.tv.restricted ? 'text-red-500' : 'text-gray-300'
}`}
>
{quota.tv.limit
? intl.formatMessage(messages.pastdays, {
type: intl.formatMessage(messages.seriesrequest),
days: quota?.tv.days,
})
: intl.formatMessage(messages.seriesrequest)}
</dt>
<dd
className={`flex items-center mt-1 text-sm ${
quota.tv.restricted ? 'text-red-500' : 'text-white'
}`}
>
{quota.tv.limit ? (
<>
<ProgressCircle
progress={Math.max(
0,
Math.round(
((quota?.tv.remaining ?? 0) /
(quota?.tv.limit ?? 1)) *
100
)
)}
useHeatLevel
className="w-8 h-8 mr-2"
/>
<div>
{intl.formatMessage(messages.requestsperdays, {
limit: (
<span className="text-3xl font-semibold">
{intl.formatMessage(messages.limit, {
remaining: quota.tv.remaining,
limit: quota.tv.limit,
})}
</span>
),
})}
</div>
</>
) : (
<span className="text-3xl">
{intl.formatMessage(messages.unlimited)}
</span>
)}
</dd>
</div>
</dl>
</div>
)}
<div className="relative z-40 mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<div className="inline-flex items-center text-xl leading-7 text-gray-300 cursor-default sm:text-2xl sm:leading-9 sm:truncate">