mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
Merge remote-tracking branch 'overseerr/develop' into develop
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import type { User } from '@app/hooks/useUser';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import { CogIcon, UserIcon } from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Permission, User, useUser } from '../../../hooks/useUser';
|
||||
import Button from '../../Common/Button';
|
||||
|
||||
const messages = defineMessages({
|
||||
settings: 'Edit Settings',
|
||||
@@ -17,10 +17,7 @@ interface ProfileHeaderProps {
|
||||
isSettingsPage?: boolean;
|
||||
}
|
||||
|
||||
const ProfileHeader: React.FC<ProfileHeaderProps> = ({
|
||||
user,
|
||||
isSettingsPage,
|
||||
}) => {
|
||||
const ProfileHeader = ({ user, isSettingsPage }: ProfileHeaderProps) => {
|
||||
const intl = useIntl();
|
||||
const { user: loggedInUser, hasPermission } = useUser();
|
||||
|
||||
@@ -61,7 +58,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
|
||||
user.id === loggedInUser?.id ? '/profile' : `/users/${user.id}`
|
||||
}
|
||||
>
|
||||
<a className="bg-gradient-to-br from-indigo-400 to-purple-400 bg-clip-text text-lg font-bold text-transparent hover:to-purple-200 sm:text-2xl">
|
||||
<a className="text-overseerr text-lg font-bold hover:to-purple-200 sm:text-2xl">
|
||||
{user.displayName}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import LanguageSelector from '@app/components/LanguageSelector';
|
||||
import QuotaSelector from '@app/components/QuotaSelector';
|
||||
import RegionSelector from '@app/components/RegionSelector';
|
||||
import type { AvailableLocale } from '@app/context/LanguageContext';
|
||||
import { availableLanguages } from '@app/context/LanguageContext';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, UserType, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsGeneralResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import {
|
||||
availableLanguages,
|
||||
AvailableLocale,
|
||||
} from '../../../../context/LanguageContext';
|
||||
import useLocale from '../../../../hooks/useLocale';
|
||||
import useSettings from '../../../../hooks/useSettings';
|
||||
import { Permission, UserType, useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Error from '../../../../pages/_error';
|
||||
import Badge from '../../../Common/Badge';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../../Common/PageTitle';
|
||||
import LanguageSelector from '../../../LanguageSelector';
|
||||
import QuotaSelector from '../../../QuotaSelector';
|
||||
import RegionSelector from '../../../RegionSelector';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
const messages = defineMessages({
|
||||
general: 'General',
|
||||
@@ -56,9 +54,15 @@ const messages = defineMessages({
|
||||
discordIdTip:
|
||||
'The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account',
|
||||
validationDiscordId: 'You must provide a valid Discord user ID',
|
||||
plexwatchlistsyncmovies: 'Auto-Request Movies',
|
||||
plexwatchlistsyncmoviestip:
|
||||
'Automatically request movies on your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>',
|
||||
plexwatchlistsyncseries: 'Auto-Request Series',
|
||||
plexwatchlistsyncseriestip:
|
||||
'Automatically request series on your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink>',
|
||||
});
|
||||
|
||||
const UserGeneralSettings: React.FC = () => {
|
||||
const UserGeneralSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { addToast } = useToasts();
|
||||
@@ -86,7 +90,7 @@ const UserGeneralSettings: React.FC = () => {
|
||||
const UserGeneralSettingsSchema = Yup.object().shape({
|
||||
discordId: Yup.string()
|
||||
.nullable()
|
||||
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -131,6 +135,8 @@ const UserGeneralSettings: React.FC = () => {
|
||||
movieQuotaDays: data?.movieQuotaDays,
|
||||
tvQuotaLimit: data?.tvQuotaLimit,
|
||||
tvQuotaDays: data?.tvQuotaDays,
|
||||
watchlistSyncMovies: data?.watchlistSyncMovies,
|
||||
watchlistSyncTv: data?.watchlistSyncTv,
|
||||
}}
|
||||
validationSchema={UserGeneralSettingsSchema}
|
||||
enableReinitialize
|
||||
@@ -149,6 +155,8 @@ const UserGeneralSettings: React.FC = () => {
|
||||
movieQuotaDays: movieQuotaEnabled ? values.movieQuotaDays : null,
|
||||
tvQuotaLimit: tvQuotaEnabled ? values.tvQuotaLimit : null,
|
||||
tvQuotaDays: tvQuotaEnabled ? values.tvQuotaDays : null,
|
||||
watchlistSyncMovies: values.watchlistSyncMovies,
|
||||
watchlistSyncTv: values.watchlistSyncTv,
|
||||
});
|
||||
|
||||
if (currentUser?.id === user?.id && setLocale) {
|
||||
@@ -243,9 +251,11 @@ const UserGeneralSettings: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{errors.displayName && touched.displayName && (
|
||||
<div className="error">{errors.displayName}</div>
|
||||
)}
|
||||
{errors.displayName &&
|
||||
touched.displayName &&
|
||||
typeof errors.displayName === 'string' && (
|
||||
<div className="error">{errors.displayName}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
@@ -280,17 +290,15 @@ const UserGeneralSettings: React.FC = () => {
|
||||
{currentUser?.id === user?.id && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.discordIdTip, {
|
||||
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
FindDiscordIdLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
@@ -299,9 +307,11 @@ const UserGeneralSettings: React.FC = () => {
|
||||
<div className="form-input-field">
|
||||
<Field id="discordId" name="discordId" type="text" />
|
||||
</div>
|
||||
{errors.discordId && touched.discordId && (
|
||||
<div className="error">{errors.discordId}</div>
|
||||
)}
|
||||
{errors.discordId &&
|
||||
touched.discordId &&
|
||||
typeof errors.discordId === 'string' && (
|
||||
<div className="error">{errors.discordId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
@@ -455,6 +465,99 @@ const UserGeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasPermission(
|
||||
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_MOVIE],
|
||||
{ type: 'or' }
|
||||
) &&
|
||||
user?.userType === UserType.PLEX && (
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="watchlistSyncMovies"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span>
|
||||
{intl.formatMessage(messages.plexwatchlistsyncmovies)}
|
||||
</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(
|
||||
messages.plexwatchlistsyncmoviestip,
|
||||
{
|
||||
PlexWatchlistSupportLink: (
|
||||
msg: React.ReactNode
|
||||
) => (
|
||||
<a
|
||||
href="https://support.plex.tv/articles/universal-watchlist/"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="watchlistSyncMovies"
|
||||
name="watchlistSyncMovies"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'watchlistSyncMovies',
|
||||
!values.watchlistSyncMovies
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{hasPermission(
|
||||
[Permission.AUTO_REQUEST, Permission.AUTO_REQUEST_TV],
|
||||
{ type: 'or' }
|
||||
) &&
|
||||
user?.userType === UserType.PLEX && (
|
||||
<div className="form-row">
|
||||
<label htmlFor="watchlistSyncTv" className="checkbox-label">
|
||||
<span>
|
||||
{intl.formatMessage(messages.plexwatchlistsyncseries)}
|
||||
</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(
|
||||
messages.plexwatchlistsyncseriestip,
|
||||
{
|
||||
PlexWatchlistSupportLink: (
|
||||
msg: React.ReactNode
|
||||
) => (
|
||||
<a
|
||||
href="https://support.plex.tv/articles/universal-watchlist/"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="watchlistSyncTv"
|
||||
name="watchlistSyncTv"
|
||||
onChange={() => {
|
||||
setFieldValue(
|
||||
'watchlistSyncTv',
|
||||
!values.watchlistSyncTv
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
discordsettingssaved: 'Discord notification settings saved successfully!',
|
||||
@@ -23,7 +22,7 @@ const messages = defineMessages({
|
||||
validationDiscordId: 'You must provide a valid user ID',
|
||||
});
|
||||
|
||||
const UserNotificationsDiscord: React.FC = () => {
|
||||
const UserNotificationsDiscord = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
@@ -46,7 +45,7 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
.required(intl.formatMessage(messages.validationDiscordId)),
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
@@ -111,17 +110,15 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
{currentUser?.id === user?.id && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.discordIdTip, {
|
||||
FindDiscordIdLink: function FindDiscordIdLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
FindDiscordIdLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
@@ -130,9 +127,11 @@ const UserNotificationsDiscord: React.FC = () => {
|
||||
<div className="form-input-field">
|
||||
<Field id="discordId" name="discordId" type="text" />
|
||||
</div>
|
||||
{errors.discordId && touched.discordId && (
|
||||
<div className="error">{errors.discordId}</div>
|
||||
)}
|
||||
{errors.discordId &&
|
||||
touched.discordId &&
|
||||
typeof errors.discordId === 'string' && (
|
||||
<div className="error">{errors.discordId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '@app/components/NotificationTypeSelector';
|
||||
import { OpenPgpLink } from '@app/components/Settings/Notifications/NotificationsEmail';
|
||||
import SettingsBadge from '@app/components/Settings/SettingsBadge';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Badge from '../../../Common/Badge';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import SensitiveInput from '../../../Common/SensitiveInput';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '../../../NotificationTypeSelector';
|
||||
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
|
||||
|
||||
const messages = defineMessages({
|
||||
emailsettingssaved: 'Email notification settings saved successfully!',
|
||||
@@ -28,7 +27,7 @@ const messages = defineMessages({
|
||||
validationPgpPublicKey: 'You must provide a valid PGP public key',
|
||||
});
|
||||
|
||||
const UserEmailSettings: React.FC = () => {
|
||||
const UserEmailSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
@@ -106,9 +105,7 @@ const UserEmailSettings: React.FC = () => {
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.pgpPublicKey)}
|
||||
</span>
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.advanced)}
|
||||
</Badge>
|
||||
<SettingsBadge badgeType="advanced" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pgpPublicKeyTip, {
|
||||
OpenPgpLink: OpenPgpLink,
|
||||
@@ -126,9 +123,11 @@ const UserEmailSettings: React.FC = () => {
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
{errors.pgpKey && touched.pgpKey && (
|
||||
<div className="error">{errors.pgpKey}</div>
|
||||
)}
|
||||
{errors.pgpKey &&
|
||||
touched.pgpKey &&
|
||||
typeof errors.pgpKey === 'string' && (
|
||||
<div className="error">{errors.pgpKey}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import SensitiveInput from '../../../Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
pushbulletsettingssaved:
|
||||
@@ -24,7 +23,7 @@ const messages = defineMessages({
|
||||
validationPushbulletAccessToken: 'You must provide an access token',
|
||||
});
|
||||
|
||||
const UserPushbulletSettings: React.FC = () => {
|
||||
const UserPushbulletSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
@@ -105,20 +104,16 @@ const UserPushbulletSettings: React.FC = () => {
|
||||
{data?.pushbulletAccessToken && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushbulletAccessTokenTip, {
|
||||
PushbulletSettingsLink: function PushbulletSettingsLink(
|
||||
msg
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
href="https://www.pushbullet.com/#settings/account"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
PushbulletSettingsLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://www.pushbullet.com/#settings/account"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import useSettings from '../../../../hooks/useSettings';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||
@@ -28,7 +27,7 @@ const messages = defineMessages({
|
||||
validationPushoverUserKey: 'You must provide a valid user or group key',
|
||||
});
|
||||
|
||||
const UserPushoverSettings: React.FC = () => {
|
||||
const UserPushoverSettings = () => {
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { addToast } = useToasts();
|
||||
@@ -129,19 +128,16 @@ const UserPushoverSettings: React.FC = () => {
|
||||
<span className="label-required">*</span>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushoverApplicationTokenTip, {
|
||||
ApplicationRegistrationLink:
|
||||
function ApplicationRegistrationLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://pushover.net/api#registration"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
ApplicationRegistrationLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://pushover.net/api#registration"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
applicationTitle: settings.currentSettings.applicationTitle,
|
||||
})}
|
||||
</span>
|
||||
@@ -167,18 +163,16 @@ const UserPushoverSettings: React.FC = () => {
|
||||
{intl.formatMessage(messages.pushoverUserKey)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushoverUserKeyTip, {
|
||||
UsersGroupsLink: function UsersGroupsLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://pushover.net/api#identifiers"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
UsersGroupsLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://pushover.net/api#identifiers"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</label>
|
||||
@@ -190,9 +184,11 @@ const UserPushoverSettings: React.FC = () => {
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.pushoverUserKey && touched.pushoverUserKey && (
|
||||
<div className="error">{errors.pushoverUserKey}</div>
|
||||
)}
|
||||
{errors.pushoverUserKey &&
|
||||
touched.pushoverUserKey &&
|
||||
typeof errors.pushoverUserKey === 'string' && (
|
||||
<div className="error">{errors.pushoverUserKey}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
telegramsettingssaved: 'Telegram notification settings saved successfully!',
|
||||
@@ -25,7 +24,7 @@ const messages = defineMessages({
|
||||
validationTelegramChatId: 'You must provide a valid chat ID',
|
||||
});
|
||||
|
||||
const UserTelegramSettings: React.FC = () => {
|
||||
const UserTelegramSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
@@ -112,31 +111,25 @@ const UserTelegramSettings: React.FC = () => {
|
||||
{data?.telegramBotUsername && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.telegramChatIdTipLong, {
|
||||
TelegramBotLink: function TelegramBotLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href={`https://telegram.me/${data.telegramBotUsername}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
GetIdBotLink: function GetIdBotLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://telegram.me/get_id_bot"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code: function code(msg) {
|
||||
return <code>{msg}</code>;
|
||||
},
|
||||
TelegramBotLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href={`https://telegram.me/${data.telegramBotUsername}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
GetIdBotLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://telegram.me/get_id_bot"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
code: (msg: React.ReactNode) => <code>{msg}</code>,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
@@ -149,9 +142,11 @@ const UserTelegramSettings: React.FC = () => {
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.telegramChatId && touched.telegramChatId && (
|
||||
<div className="error">{errors.telegramChatId}</div>
|
||||
)}
|
||||
{errors.telegramChatId &&
|
||||
touched.telegramChatId &&
|
||||
typeof errors.telegramChatId === 'string' && (
|
||||
<div className="error">{errors.telegramChatId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '@app/components/NotificationTypeSelector';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector, {
|
||||
ALL_NOTIFICATIONS,
|
||||
} from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||
});
|
||||
|
||||
const UserWebPushSettings: React.FC = () => {
|
||||
const UserWebPushSettings = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import DiscordLogo from '@app/assets/extlogos/discord.svg';
|
||||
import PushbulletLogo from '@app/assets/extlogos/pushbullet.svg';
|
||||
import PushoverLogo from '@app/assets/extlogos/pushover.svg';
|
||||
import TelegramLogo from '@app/assets/extlogos/telegram.svg';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
|
||||
import SettingsTabs from '@app/components/Common/SettingsTabs';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { CloudIcon, MailIcon } from '@heroicons/react/solid';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import DiscordLogo from '../../../../assets/extlogos/discord.svg';
|
||||
import PushbulletLogo from '../../../../assets/extlogos/pushbullet.svg';
|
||||
import PushoverLogo from '../../../../assets/extlogos/pushover.svg';
|
||||
import TelegramLogo from '../../../../assets/extlogos/telegram.svg';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Error from '../../../../pages/_error';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../../Common/PageTitle';
|
||||
import SettingsTabs, { SettingsRoute } from '../../../Common/SettingsTabs';
|
||||
|
||||
const messages = defineMessages({
|
||||
notifications: 'Notifications',
|
||||
@@ -22,7 +22,13 @@ const messages = defineMessages({
|
||||
webpush: 'Web Push',
|
||||
});
|
||||
|
||||
const UserNotificationSettings: React.FC = ({ children }) => {
|
||||
type UserNotificationSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const UserNotificationSettings = ({
|
||||
children,
|
||||
}: UserNotificationSettingsProps) => {
|
||||
const intl = useIntl();
|
||||
const router = useRouter();
|
||||
const { user } = useUser({ id: Number(router.query.userId) });
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { Permission, useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Error from '../../../../pages/_error';
|
||||
import Alert from '../../../Common/Alert';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../../Common/PageTitle';
|
||||
import SensitiveInput from '../../../Common/SensitiveInput';
|
||||
|
||||
const messages = defineMessages({
|
||||
password: 'Password',
|
||||
@@ -39,7 +38,7 @@ const messages = defineMessages({
|
||||
"You do not have permission to modify this user's password.",
|
||||
});
|
||||
|
||||
const UserPasswordChange: React.FC = () => {
|
||||
const UserPasswordChange = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
@@ -176,9 +175,11 @@ const UserPasswordChange: React.FC = () => {
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{errors.currentPassword && touched.currentPassword && (
|
||||
<div className="error">{errors.currentPassword}</div>
|
||||
)}
|
||||
{errors.currentPassword &&
|
||||
touched.currentPassword &&
|
||||
typeof errors.currentPassword === 'string' && (
|
||||
<div className="error">{errors.currentPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -196,9 +197,11 @@ const UserPasswordChange: React.FC = () => {
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{errors.newPassword && touched.newPassword && (
|
||||
<div className="error">{errors.newPassword}</div>
|
||||
)}
|
||||
{errors.newPassword &&
|
||||
touched.newPassword &&
|
||||
typeof errors.newPassword === 'string' && (
|
||||
<div className="error">{errors.newPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
@@ -215,9 +218,11 @@ const UserPasswordChange: React.FC = () => {
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{errors.confirmPassword && touched.confirmPassword && (
|
||||
<div className="error">{errors.confirmPassword}</div>
|
||||
)}
|
||||
{errors.confirmPassword &&
|
||||
touched.confirmPassword &&
|
||||
typeof errors.confirmPassword === 'string' && (
|
||||
<div className="error">{errors.confirmPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PermissionEdit from '@app/components/PermissionEdit';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { SaveIcon } from '@heroicons/react/outline';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Error from '../../../../pages/_error';
|
||||
import Alert from '../../../Common/Alert';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../../Common/PageTitle';
|
||||
import PermissionEdit from '../../../PermissionEdit';
|
||||
|
||||
const messages = defineMessages({
|
||||
toastSettingsSuccess: 'Permissions saved successfully!',
|
||||
@@ -22,7 +21,7 @@ const messages = defineMessages({
|
||||
unauthorizedDescription: 'You cannot modify your own permissions.',
|
||||
});
|
||||
|
||||
const UserPermissions: React.FC = () => {
|
||||
const UserPermissions = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
|
||||
import SettingsTabs from '@app/components/Common/SettingsTabs';
|
||||
import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { hasPermission, Permission } from '../../../../server/lib/permissions';
|
||||
import useSettings from '../../../hooks/useSettings';
|
||||
import { useUser } from '../../../hooks/useUser';
|
||||
import globalMessages from '../../../i18n/globalMessages';
|
||||
import Error from '../../../pages/_error';
|
||||
import Alert from '../../Common/Alert';
|
||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../Common/PageTitle';
|
||||
import SettingsTabs, { SettingsRoute } from '../../Common/SettingsTabs';
|
||||
import ProfileHeader from '../ProfileHeader';
|
||||
|
||||
const messages = defineMessages({
|
||||
menuGeneralSettings: 'General',
|
||||
@@ -23,7 +23,11 @@ const messages = defineMessages({
|
||||
"You do not have permission to modify this user's settings.",
|
||||
});
|
||||
|
||||
const UserSettings: React.FC = ({ children }) => {
|
||||
type UserSettingsProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const UserSettings = ({ children }: UserSettingsProps) => {
|
||||
const router = useRouter();
|
||||
const settings = useSettings();
|
||||
const { user: currentUser } = useUser();
|
||||
@@ -53,10 +57,7 @@ const UserSettings: React.FC = ({ children }) => {
|
||||
regex: /\/settings\/password/,
|
||||
hidden:
|
||||
(!settings.currentSettings.localLogin &&
|
||||
!hasPermission(
|
||||
Permission.MANAGE_SETTINGS,
|
||||
currentUser?.permissions ?? 0
|
||||
)) ||
|
||||
!hasPermission(Permission.ADMIN, currentUser?.permissions ?? 0)) ||
|
||||
(currentUser?.id !== 1 &&
|
||||
currentUser?.id !== user?.id &&
|
||||
hasPermission(Permission.ADMIN, user?.permissions ?? 0)),
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import ImageFader from '@app/components/Common/ImageFader';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import ProgressCircle from '@app/components/Common/ProgressCircle';
|
||||
import RequestCard from '@app/components/RequestCard';
|
||||
import Slider from '@app/components/Slider';
|
||||
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
|
||||
import ProfileHeader from '@app/components/UserProfile/ProfileHeader';
|
||||
import { Permission, UserType, useUser } from '@app/hooks/useUser';
|
||||
import Error from '@app/pages/_error';
|
||||
import { ArrowCircleRightIcon } from '@heroicons/react/outline';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
import type { WatchlistResponse } from '@server/interfaces/api/discoverInterfaces';
|
||||
import type {
|
||||
QuotaResponse,
|
||||
UserRequestsResponse,
|
||||
UserWatchDataResponse,
|
||||
} from '../../../server/interfaces/api/userInterfaces';
|
||||
import { MovieDetails } from '../../../server/models/Movie';
|
||||
import { TvDetails } from '../../../server/models/Tv';
|
||||
import { Permission, UserType, 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 TmdbTitleCard from '../TitleCard/TmdbTitleCard';
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
} from '@server/interfaces/api/userInterfaces';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
recentrequests: 'Recent Requests',
|
||||
norequests: 'No requests.',
|
||||
limit: '{remaining} of {limit}',
|
||||
requestsperdays: '{limit} remaining',
|
||||
unlimited: 'Unlimited',
|
||||
@@ -33,11 +33,14 @@ const messages = defineMessages({
|
||||
movierequests: 'Movie Requests',
|
||||
seriesrequest: 'Series Requests',
|
||||
recentlywatched: 'Recently Watched',
|
||||
plexwatchlist: 'Plex Watchlist',
|
||||
emptywatchlist:
|
||||
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
|
||||
});
|
||||
|
||||
type MediaTitle = MovieDetails | TvDetails;
|
||||
|
||||
const UserProfile: React.FC = () => {
|
||||
const UserProfile = () => {
|
||||
const intl = useIntl();
|
||||
const router = useRouter();
|
||||
const { user, error } = useUser({
|
||||
@@ -68,12 +71,29 @@ const UserProfile: React.FC = () => {
|
||||
? `/api/v1/user/${user.id}/quota`
|
||||
: null
|
||||
);
|
||||
const { data: watchData } = useSWR<UserWatchDataResponse>(
|
||||
user?.userType === UserType.PLEX &&
|
||||
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
|
||||
? `/api/v1/user/${user.id}/watch_data`
|
||||
: null
|
||||
);
|
||||
const { data: watchData, error: watchDataError } =
|
||||
useSWR<UserWatchDataResponse>(
|
||||
user?.userType === UserType.PLEX &&
|
||||
(user.id === currentUser?.id || currentHasPermission(Permission.ADMIN))
|
||||
? `/api/v1/user/${user.id}/watch_data`
|
||||
: null
|
||||
);
|
||||
const { data: watchlistItems, error: watchlistError } =
|
||||
useSWR<WatchlistResponse>(
|
||||
user?.userType === UserType.PLEX &&
|
||||
(user.id === currentUser?.id ||
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
))
|
||||
? `/api/v1/user/${user.id}/watchlist`
|
||||
: null,
|
||||
{
|
||||
revalidateOnMount: true,
|
||||
}
|
||||
);
|
||||
|
||||
const updateAvailableTitles = useCallback(
|
||||
(requestId: number, mediaTitle: MediaTitle) => {
|
||||
@@ -129,7 +149,15 @@ const UserProfile: React.FC = () => {
|
||||
{intl.formatMessage(messages.totalrequests)}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold text-white">
|
||||
<FormattedNumber value={user.requestCount} />
|
||||
<Link
|
||||
href={
|
||||
user.id === currentUser?.id
|
||||
? '/profile/requests?filter=all'
|
||||
: `/users/${user?.id}/requests?filter=all`
|
||||
}
|
||||
>
|
||||
<a>{intl.formatNumber(user.requestCount)}</a>
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
@@ -249,37 +277,97 @@ const UserProfile: React.FC = () => {
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
)) && (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href={`/users/${user?.id}/requests?filter=all`}>
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.recentrequests)}</span>
|
||||
<ArrowCircleRightIcon />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
sliderKey="requests"
|
||||
isLoading={!requests && !requestError}
|
||||
isEmpty={
|
||||
!!requests && !requestError && requests.results.length === 0
|
||||
}
|
||||
items={(requests?.results ?? []).map((request) => (
|
||||
<RequestCard
|
||||
key={`request-slider-item-${request.id}`}
|
||||
request={request}
|
||||
onTitleData={updateAvailableTitles}
|
||||
/>
|
||||
))}
|
||||
placeholder={<RequestCard.Placeholder />}
|
||||
emptyMessage={intl.formatMessage(messages.norequests)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(user.id === currentUser?.id ||
|
||||
currentHasPermission(Permission.ADMIN)) &&
|
||||
!!watchData?.recentlyWatched.length && (
|
||||
)) &&
|
||||
(!requests || !!requests.results.length) &&
|
||||
!requestError && (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link
|
||||
href={
|
||||
user.id === currentUser?.id
|
||||
? '/profile/requests?filter=all'
|
||||
: `/users/${user?.id}/requests?filter=all`
|
||||
}
|
||||
>
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.recentrequests)}</span>
|
||||
<ArrowCircleRightIcon />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
sliderKey="requests"
|
||||
isLoading={!requests}
|
||||
items={(requests?.results ?? []).map((request) => (
|
||||
<RequestCard
|
||||
key={`request-slider-item-${request.id}`}
|
||||
request={request}
|
||||
onTitleData={updateAvailableTitles}
|
||||
/>
|
||||
))}
|
||||
placeholder={<RequestCard.Placeholder />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{user.userType === UserType.PLEX &&
|
||||
(user.id === currentUser?.id ||
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.WATCHLIST_VIEW],
|
||||
{ type: 'or' }
|
||||
)) &&
|
||||
(!watchlistItems ||
|
||||
!!watchlistItems.results.length ||
|
||||
(user.id === currentUser?.id &&
|
||||
(user.settings?.watchlistSyncMovies ||
|
||||
user.settings?.watchlistSyncTv))) &&
|
||||
!watchlistError && (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link
|
||||
href={
|
||||
user.id === currentUser?.id
|
||||
? '/profile/watchlist'
|
||||
: `/users/${user?.id}/watchlist`
|
||||
}
|
||||
>
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.plexwatchlist)}</span>
|
||||
<ArrowCircleRightIcon />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
sliderKey="watchlist"
|
||||
isLoading={!watchlistItems}
|
||||
isEmpty={!!watchlistItems && watchlistItems.results.length === 0}
|
||||
emptyMessage={intl.formatMessage(messages.emptywatchlist, {
|
||||
PlexWatchlistSupportLink: (msg: React.ReactNode) => (
|
||||
<a
|
||||
href="https://support.plex.tv/articles/universal-watchlist/"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
items={watchlistItems?.results.map((item) => (
|
||||
<TmdbTitleCard
|
||||
id={item.tmdbId}
|
||||
key={`watchlist-slider-item-${item.ratingKey}`}
|
||||
tmdbId={item.tmdbId}
|
||||
type={item.mediaType}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{user.userType === UserType.PLEX &&
|
||||
(user.id === currentUser?.id ||
|
||||
currentHasPermission(Permission.ADMIN)) &&
|
||||
(!watchData || !!watchData.recentlyWatched.length) &&
|
||||
!watchDataError && (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<div className="slider-title">
|
||||
@@ -289,11 +377,12 @@ const UserProfile: React.FC = () => {
|
||||
<Slider
|
||||
sliderKey="media"
|
||||
isLoading={!watchData}
|
||||
isEmpty={!watchData?.recentlyWatched.length}
|
||||
items={watchData.recentlyWatched.map((item) => (
|
||||
items={watchData?.recentlyWatched.map((item) => (
|
||||
<TmdbTitleCard
|
||||
key={`media-slider-item-${item.id}`}
|
||||
id={item.id}
|
||||
tmdbId={item.tmdbId}
|
||||
tvdbId={item.tvdbId}
|
||||
type={item.mediaType}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user