mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 20:28:40 -05:00
refactor: update Next.js, React.js and Node.js (#815)
* refactor: update Next.js and React.js * refactor: update Next.js images * refactor: update ESLint rules and fix warnings/errors * fix: remove old intl polyfill * fix: add proper size to next/image components * fix: adjust full-size for next/image components * fix: temporary allow all domains for image optimization * build: fixes an issue where dev env could lead to javascript heap out of memory * fix: resolve webpack cache issue with country-flag-icons * refactor: switch compiler from Babel to SWC * fix: resize logo in sidebar * fix: break word on long path to avoid text overflow * chore: added sharp for production image optimisation * fix: change extract script for i18n to a custom script * fix: resolve GitHub CodeQL alert * chore: temporarily remove builds for ARMv7 * fix: resize avatar images * refactor: update Node.js to v20 * fix: resolve various UI issues * build: migrate yarn to pnpm and restrict engine to node@^20.0.0 * ci: specify the pnpm version to use in workflow actions * ci: fix typo in pnpm action-setup for cypress workflow * test(cypress): use pnpm instead of yarn * style: ran prettier on pnpm-lock * ci(cypress): setup nodejs v20 in cypress workflow * ci: pnpm cache to reduce install time * ci: use sh shell to get pnpm store directory * build(dockerfile): migrate to pnpm from yarn in docker builds * build(dockerfile): copy the proper pnpm lockfile * build: install pnpm for all platforms * build(dockerfile): remove unnecessary `&&` on apk installation steps * build: migrate pnpm 8 to 9 * build(dockerfile): add node-gyp back in * build(dockerfile): install node-gyp through npm * build(dockerfile): ignore scripts to not run husky install when devdependencies are pruned * build: migrate to pnpm from yarn * chore: remove a section that is no longer relevant --------- Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.AirDateBadge', {
|
||||
airedrelative: 'Aired {relativeTime}',
|
||||
airsrelative: 'Airing {relativeTime}',
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.AppDataWarning', {
|
||||
dockerVolumeMissingDescription:
|
||||
'The <code>{appDataPath}</code> volume mount was not configured properly. All data will be cleared when the container is stopped or restarted.',
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
@@ -18,10 +19,10 @@ import { uniq } from 'lodash';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.CollectionDetails', {
|
||||
overview: 'Overview',
|
||||
numberofmovies: '{count} Movies',
|
||||
requestcollection: 'Request Collection',
|
||||
@@ -166,10 +167,9 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
<Link
|
||||
href={`/discover/movies/genre/${genreId}`}
|
||||
key={`genre-${genreId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
<a className="hover:underline">
|
||||
{genres.find((g) => g.id === genreId)?.name}
|
||||
</a>
|
||||
{genres.find((g) => g.id === genreId)?.name}
|
||||
</Link>
|
||||
))
|
||||
.reduce((prev, curr) => (
|
||||
@@ -195,8 +195,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
@@ -229,7 +229,8 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
|
||||
@@ -93,13 +93,12 @@ const Badge = (
|
||||
);
|
||||
} else if (href) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a
|
||||
className={badgeStyle.join(' ')}
|
||||
ref={ref as React.Ref<HTMLAnchorElement>}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
<Link
|
||||
href={href}
|
||||
className={badgeStyle.join(' ')}
|
||||
ref={ref as React.Ref<HTMLAnchorElement>}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -64,8 +64,8 @@ const ImageFader: ForwardRefRenderFunction<HTMLDivElement, ImageFaderProps> = (
|
||||
className="absolute inset-0 h-full w-full"
|
||||
alt=""
|
||||
src={imageUrl}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
{...overrides}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -125,8 +125,8 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={backdrop}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -55,15 +55,14 @@ const SettingsLink = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={route}>
|
||||
<a
|
||||
className={`${linkClasses} ${
|
||||
currentPath.match(regex) ? activeLinkColor : inactiveLinkColor
|
||||
}`}
|
||||
aria-current="page"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
<Link
|
||||
href={route}
|
||||
className={`${linkClasses} ${
|
||||
currentPath.match(regex) ? activeLinkColor : inactiveLinkColor
|
||||
}`}
|
||||
aria-current="page"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,40 +12,39 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<a
|
||||
className={`relative flex h-32 w-56 transform-gpu cursor-pointer items-center justify-center p-8 shadow ring-1 transition duration-300 ease-in-out sm:h-36 sm:w-72 ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
} rounded-xl`}
|
||||
onMouseEnter={() => {
|
||||
<Link
|
||||
href={url}
|
||||
className={`relative flex h-32 w-56 transform-gpu cursor-pointer items-center justify-center p-8 shadow ring-1 transition duration-300 ease-in-out sm:h-36 sm:w-72 ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
} rounded-xl`}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative h-full w-full">
|
||||
<CachedImage
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 h-full w-full"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
}`}
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative h-full w-full">
|
||||
<CachedImage
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 h-full w-full"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
fill
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
}`}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sliderTitles } from '@app/components/Discover/constants';
|
||||
import MediaSlider from '@app/components/MediaSlider';
|
||||
import { WatchProviderSelector } from '@app/components/Selector';
|
||||
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type {
|
||||
TmdbCompanySearchResponse,
|
||||
TmdbGenre,
|
||||
@@ -16,12 +17,12 @@ import type { Keyword, ProductionCompany } from '@server/models/common';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.CreateSlider', {
|
||||
addSlider: 'Add Slider',
|
||||
editSlider: 'Edit Slider',
|
||||
slidernameplaceholder: 'Slider Name',
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverMovieGenre', {
|
||||
genreMovies: '{genre} Movies',
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverMovieKeyword', {
|
||||
keywordMovies: '{keywordTitle} Movies',
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverMovieLanguage', {
|
||||
languageMovies: '{language} Movies',
|
||||
});
|
||||
|
||||
|
||||
@@ -11,14 +11,15 @@ import FilterSlideover from '@app/components/Discover/FilterSlideover';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverMovies', {
|
||||
discovermovies: 'Movies',
|
||||
activefilters:
|
||||
'{count, plural, one {# Active Filter} other {# Active Filters}}',
|
||||
|
||||
@@ -4,12 +4,14 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvNetwork } from '@server/models/common';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverNetwork', {
|
||||
networkSeries: '{network} Series',
|
||||
});
|
||||
|
||||
@@ -47,10 +49,11 @@ const DiscoverTvNetwork = () => {
|
||||
<Header>
|
||||
{firstResultData?.network.logoPath ? (
|
||||
<div className="mb-6 flex justify-center">
|
||||
<img
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.network.logoPath}`}
|
||||
alt={firstResultData.network.name}
|
||||
className="max-h-24 sm:max-h-32"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -8,6 +8,7 @@ import CreateSlider from '@app/components/Discover/CreateSlider';
|
||||
import GenreTag from '@app/components/GenreTag';
|
||||
import KeywordTag from '@app/components/KeywordTag';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ArrowUturnLeftIcon,
|
||||
@@ -22,10 +23,10 @@ import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import axios from 'axios';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDrag, useDrop } from 'react-aria';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverSliderEdit', {
|
||||
deletesuccess: 'Sucessfully deleted slider.',
|
||||
deletefail: 'Failed to delete slider.',
|
||||
remove: 'Remove',
|
||||
|
||||
@@ -4,12 +4,14 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { ProductionCompany } from '@server/models/common';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverStudio', {
|
||||
studioMovies: '{studio} Movies',
|
||||
});
|
||||
|
||||
@@ -47,10 +49,11 @@ const DiscoverMovieStudio = () => {
|
||||
<Header>
|
||||
{firstResultData?.studio.logoPath ? (
|
||||
<div className="mb-6 flex justify-center">
|
||||
<img
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)${firstResultData.studio.logoPath}`}
|
||||
alt={firstResultData.studio.name}
|
||||
className="max-h-24 sm:max-h-32"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -11,14 +11,15 @@ import FilterSlideover from '@app/components/Discover/FilterSlideover';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { BarsArrowDownIcon, FunnelIcon } from '@heroicons/react/24/solid';
|
||||
import type { SortOptions as TMDBSortOptions } from '@server/api/themoviedb';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverTv', {
|
||||
discovertv: 'Series',
|
||||
activefilters:
|
||||
'{count, plural, one {# Active Filter} other {# Active Filters}}',
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverTvGenre', {
|
||||
genreSeries: '{genre} Series',
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover, { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverTvKeyword', {
|
||||
keywordSeries: '{keywordTitle} Series',
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverTvLanguage', {
|
||||
languageSeries: '{language} Series',
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { TvResult } from '@server/models/Search';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
upcomingtv: 'Upcoming Series',
|
||||
});
|
||||
const messages = defineMessages('components.DiscoverTvUpcoming', {});
|
||||
|
||||
const DiscoverTvUpcoming = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -4,12 +4,13 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.DiscoverWatchlist', {
|
||||
discoverwatchlist: 'Your Watchlist',
|
||||
watchlist: 'Plex Watchlist',
|
||||
});
|
||||
@@ -58,8 +59,8 @@ const DiscoverWatchlist = () => {
|
||||
<Header
|
||||
subtext={
|
||||
router.query.userId ? (
|
||||
<Link href={`/users/${user?.id}`}>
|
||||
<a className="hover:underline">{user?.displayName}</a>
|
||||
<Link href={`/users/${user?.id}`} className="hover:underline">
|
||||
{user?.displayName}
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
useBatchUpdateQueryParams,
|
||||
useUpdateQueryParams,
|
||||
} from '@app/hooks/useUpdateQueryParams';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { XCircleIcon } from '@heroicons/react/24/outline';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Datepicker from 'react-tailwindcss-datepicker-sct';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.FilterSlideover', {
|
||||
filters: 'Filters',
|
||||
activefilters:
|
||||
'{count, plural, one {# Active Filter} other {# Active Filters}}',
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.MovieGenreList', {
|
||||
moviegenres: 'Movie Genres',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Slider from '@app/components/Slider';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.MovieGenreSlider', {
|
||||
moviegenres: 'Movie Genres',
|
||||
});
|
||||
|
||||
@@ -25,11 +26,9 @@ const MovieGenreSlider = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href="/discover/movies/genres">
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.moviegenres)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link href="/discover/movies/genres" className="slider-title">
|
||||
<span>{intl.formatMessage(messages.moviegenres)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import CompanyCard from '@app/components/CompanyCard';
|
||||
import Slider from '@app/components/Slider';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.NetworkSlider', {
|
||||
networks: 'Networks',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import Slider from '@app/components/Slider';
|
||||
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
|
||||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.PlexWatchlistSlider', {
|
||||
plexwatchlist: 'Your Watchlist',
|
||||
emptywatchlist:
|
||||
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
|
||||
@@ -39,11 +40,9 @@ const PlexWatchlistSlider = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href="/discover/watchlist">
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.plexwatchlist)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link href="/discover/watchlist" className="slider-title">
|
||||
<span>{intl.formatMessage(messages.plexwatchlist)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
|
||||
@@ -24,11 +24,9 @@ const RecentRequestsSlider = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href="/requests?filter=all">
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(sliderTitles.recentrequests)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link href="/requests?filter=all" className="slider-title">
|
||||
<span>{intl.formatMessage(sliderTitles.recentrequests)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Slider from '@app/components/Slider';
|
||||
import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.RecentlyAddedSlider', {
|
||||
recentlyAdded: 'Recently Added',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import CompanyCard from '@app/components/CompanyCard';
|
||||
import Slider from '@app/components/Slider';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.StudioSlider', {
|
||||
studios: 'Studios',
|
||||
});
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type {
|
||||
MovieResult,
|
||||
PersonResult,
|
||||
TvResult,
|
||||
} from '@server/models/Search';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover', {
|
||||
trending: 'Trending',
|
||||
});
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.TvGenreList', {
|
||||
seriesgenres: 'Series Genres',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { genreColorMap } from '@app/components/Discover/constants';
|
||||
import GenreCard from '@app/components/GenreCard';
|
||||
import Slider from '@app/components/Slider';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/outline';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover.TvGenreSlider', {
|
||||
tvgenres: 'Series Genres',
|
||||
});
|
||||
|
||||
@@ -25,11 +26,9 @@ const TvGenreSlider = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href="/discover/tv/genres">
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.tvgenres)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link href="/discover/tv/genres" className="slider-title">
|
||||
<span>{intl.formatMessage(messages.tvgenres)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
|
||||
@@ -3,10 +3,11 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover', {
|
||||
upcomingmovies: 'Upcoming Movies',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { ParsedUrlQuery } from 'querystring';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { z } from 'zod';
|
||||
|
||||
type AvailableColors =
|
||||
@@ -66,7 +66,7 @@ export const genreColorMap: Record<number, [string, string]> = {
|
||||
10768: colorTones.darkred, // War & Politics
|
||||
};
|
||||
|
||||
export const sliderTitles = defineMessages({
|
||||
export const sliderTitles = defineMessages('components.Discover', {
|
||||
recentrequests: 'Recent Requests',
|
||||
popularmovies: 'Popular Movies',
|
||||
populartv: 'Popular Series',
|
||||
|
||||
@@ -17,6 +17,7 @@ import MediaSlider from '@app/components/MediaSlider';
|
||||
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
ArrowDownOnSquareIcon,
|
||||
@@ -29,11 +30,11 @@ import { DiscoverSliderType } from '@server/constants/discover';
|
||||
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import axios from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Discover', {
|
||||
discover: 'Discover',
|
||||
emptywatchlist:
|
||||
'Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.DownloadBlock', {
|
||||
estimatedtime: 'Estimated {time}',
|
||||
formattedTitle: '{title}: Season {seasonNumber} Episode {episodeNumber}',
|
||||
});
|
||||
|
||||
@@ -14,37 +14,41 @@ const GenreCard = ({ image, url, name, canExpand = false }: GenreCardProps) => {
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<a
|
||||
className={`relative flex h-32 items-center justify-center sm:h-36 ${
|
||||
canExpand ? 'w-full' : 'w-56 sm:w-72'
|
||||
} transform-gpu cursor-pointer p-8 shadow ring-1 transition duration-300 ease-in-out ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 bg-opacity-100 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 bg-opacity-80 ring-gray-700'
|
||||
} overflow-hidden rounded-xl bg-cover bg-center`}
|
||||
onMouseEnter={() => {
|
||||
<Link
|
||||
href={url}
|
||||
className={`relative flex h-32 items-center justify-center sm:h-36 ${
|
||||
canExpand ? 'w-full' : 'w-56 sm:w-72'
|
||||
} transform-gpu cursor-pointer p-8 shadow ring-1 transition duration-300 ease-in-out ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 bg-opacity-100 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 bg-opacity-80 ring-gray-700'
|
||||
} overflow-hidden rounded-xl bg-cover bg-center`}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<CachedImage src={image} alt="" layout="fill" objectFit="cover" />
|
||||
<div
|
||||
className={`absolute inset-0 z-10 h-full w-full bg-gray-800 transition duration-300 ${
|
||||
isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
|
||||
}`}
|
||||
/>
|
||||
<div className="relative z-20 w-full truncate whitespace-normal text-center text-2xl font-bold text-white sm:text-3xl">
|
||||
{name}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<CachedImage
|
||||
src={image}
|
||||
alt=""
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 z-10 h-full w-full bg-gray-800 transition duration-300 ${
|
||||
isHovered ? 'bg-opacity-10' : 'bg-opacity-30'
|
||||
}`}
|
||||
/>
|
||||
<div className="relative z-20 w-full truncate whitespace-normal text-center text-2xl font-bold text-white sm:text-3xl">
|
||||
{name}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,10 +45,9 @@ const IssueBlock = ({ issue }: IssueBlockProps) => {
|
||||
? '/profile'
|
||||
: `/users/${issue.createdBy.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{issue.createdBy.displayName}
|
||||
</a>
|
||||
{issue.createdBy.displayName}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
@@ -64,7 +63,7 @@ const IssueBlock = ({ issue }: IssueBlockProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 flex flex-shrink-0 flex-wrap">
|
||||
<Link href={`/issues/${issue.id}`} passHref>
|
||||
<Link href={`/issues/${issue.id}`} passHref legacyBehavior>
|
||||
<Button buttonType="primary" as="a">
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
||||
import type { default as IssueCommentType } from '@server/entity/IssueComment';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueDetails.IssueComment', {
|
||||
postedby: 'Posted {relativeTime} by {username}',
|
||||
postedbyedited: 'Posted {relativeTime} by {username} (Edited)',
|
||||
delete: 'Delete Comment',
|
||||
@@ -84,13 +86,13 @@ const IssueComment = ({
|
||||
</Modal>
|
||||
</Transition>
|
||||
<Link href={isActiveUser ? '/profile' : `/users/${comment.user.id}`}>
|
||||
<a>
|
||||
<img
|
||||
src={comment.user.avatar}
|
||||
alt=""
|
||||
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
/>
|
||||
</a>
|
||||
<Image
|
||||
src={comment.user.avatar}
|
||||
alt=""
|
||||
className="h-10 w-10 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</Link>
|
||||
<div className="relative flex-1">
|
||||
<div className="w-full rounded-md shadow ring-1 ring-gray-500">
|
||||
@@ -242,10 +244,9 @@ const IssueComment = ({
|
||||
href={
|
||||
isActiveUser ? '/profile' : `/users/${comment.user.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{comment.user.displayName}
|
||||
</a>
|
||||
{comment.user.displayName}
|
||||
</Link>
|
||||
),
|
||||
relativeTime: (
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueDetails.IssueDescription', {
|
||||
description: 'Description',
|
||||
edit: 'Edit Description',
|
||||
deleteissue: 'Delete Issue',
|
||||
|
||||
@@ -12,6 +12,7 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
@@ -29,15 +30,16 @@ import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueDetails', {
|
||||
openedby: '#{issueId} opened {relativeTime} by {username}',
|
||||
closeissue: 'Close Issue',
|
||||
closeissueandcomment: 'Close with Comment',
|
||||
@@ -210,8 +212,8 @@ const IssueDetails = () => {
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
@@ -232,7 +234,8 @@ const IssueDetails = () => {
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
@@ -256,8 +259,9 @@ const IssueDetails = () => {
|
||||
href={`/${
|
||||
issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv'
|
||||
}/${data.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
<a className="hover:underline">{title}</a>
|
||||
{title}
|
||||
</Link>{' '}
|
||||
{releaseYear && (
|
||||
<span className="media-year">({releaseYear.slice(0, 4)})</span>
|
||||
@@ -273,17 +277,18 @@ const IssueDetails = () => {
|
||||
? '/profile'
|
||||
: `/users/${issueData.createdBy.id}`
|
||||
}
|
||||
className="group ml-1 inline-flex h-full items-center xl:ml-1.5"
|
||||
>
|
||||
<a className="group ml-1 inline-flex h-full items-center xl:ml-1.5">
|
||||
<img
|
||||
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
||||
src={issueData.createdBy.avatar}
|
||||
alt=""
|
||||
/>
|
||||
<span className="font-semibold text-gray-100 transition duration-300 group-hover:text-white group-hover:underline">
|
||||
{issueData.createdBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
<Image
|
||||
className="mr-0.5 h-5 w-5 scale-100 transform-gpu rounded-full object-cover transition duration-300 group-hover:scale-105 xl:mr-1 xl:h-6 xl:w-6"
|
||||
src={issueData.createdBy.avatar}
|
||||
alt=""
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="font-semibold text-gray-100 transition duration-300 group-hover:text-white group-hover:underline">
|
||||
{issueData.createdBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
relativeTime: (
|
||||
|
||||
@@ -4,18 +4,20 @@ import CachedImage from '@app/components/Common/CachedImage';
|
||||
import { issueOptions } from '@app/components/IssueModal/constants';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { EyeIcon } from '@heroicons/react/24/solid';
|
||||
import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaType } from '@server/constants/media';
|
||||
import type Issue from '@server/entity/Issue';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueList.IssueItem', {
|
||||
openeduserdate: '{date} by {user}',
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
episodes: '{episodeCount, plural, one {Episode} other {Episodes}}',
|
||||
@@ -113,8 +115,8 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -133,21 +135,20 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
||||
? `/movie/${issue.media.tmdbId}`
|
||||
: `/tv/${issue.media.tmdbId}`
|
||||
}
|
||||
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
||||
>
|
||||
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</a>
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div className="pt-0.5 text-xs text-white sm:pt-1">
|
||||
@@ -162,10 +163,9 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
||||
? `/movie/${issue.media.tmdbId}`
|
||||
: `/tv/${issue.media.tmdbId}`
|
||||
}
|
||||
className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"
|
||||
>
|
||||
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
{problemSeasonEpisodeLine.length > 0 && (
|
||||
<div className="card-field">
|
||||
@@ -222,17 +222,20 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${issue.createdBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={issue.createdBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5 object-cover"
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{issue.createdBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
<Link
|
||||
href={`/users/${issue.createdBy.id}`}
|
||||
className="group flex items-center truncate"
|
||||
>
|
||||
<Image
|
||||
src={issue.createdBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5 object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{issue.createdBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
@@ -259,7 +262,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
||||
</div>
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
<span className="w-full">
|
||||
<Link href={`/issues/${issue.id}`} passHref>
|
||||
<Link href={`/issues/${issue.id}`} passHref legacyBehavior>
|
||||
<Button as="a" className="w-full" buttonType="primary">
|
||||
<EyeIcon />
|
||||
<span>{intl.formatMessage(messages.viewissue)}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import IssueItem from '@app/components/IssueList/IssueItem';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
BarsArrowDownIcon,
|
||||
ChevronLeftIcon,
|
||||
@@ -14,10 +15,10 @@ import {
|
||||
import type { IssueResultsResponse } from '@server/interfaces/api/issueInterfaces';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueList', {
|
||||
issues: 'Issues',
|
||||
sortAdded: 'Most Recent',
|
||||
sortModified: 'Last Modified',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { issueOptions } from '@app/components/IssueModal/constants';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { RadioGroup } from '@headlessui/react';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
@@ -13,12 +14,12 @@ import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueModal.CreateIssueModal', {
|
||||
validationMessageRequired: 'You must provide a description',
|
||||
whatswrong: "What's wrong?",
|
||||
providedetail:
|
||||
@@ -118,7 +119,7 @@ const CreateIssueModal = ({
|
||||
strong: (msg: React.ReactNode) => <strong>{msg}</strong>,
|
||||
})}
|
||||
</div>
|
||||
<Link href={`/issues/${newIssue.data.id}`}>
|
||||
<Link href={`/issues/${newIssue.data.id}`} legacyBehavior>
|
||||
<Button as="a" className="mt-4">
|
||||
<span>{intl.formatMessage(messages.toastviewissue)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { IssueType } from '@server/constants/issue';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.IssueModal', {
|
||||
issueAudio: 'Audio',
|
||||
issueVideo: 'Video',
|
||||
issueSubtitles: 'Subtitle',
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { Language } from '@server/lib/settings';
|
||||
import { sortBy } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import type { CSSObjectWithLabel } from 'react-select';
|
||||
import Select from 'react-select';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.LanguageSelector', {
|
||||
originalLanguageDefault: 'All Languages',
|
||||
languageServerDefault: 'Default ({language})',
|
||||
});
|
||||
|
||||
@@ -2,12 +2,13 @@ import type { AvailableLocale } from '@app/context/LanguageContext';
|
||||
import { availableLanguages } from '@app/context/LanguageContext';
|
||||
import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { LanguageIcon } from '@heroicons/react/24/solid';
|
||||
import { useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Layout.LanguagePicker', {
|
||||
displaylanguage: 'Display Language',
|
||||
});
|
||||
|
||||
|
||||
@@ -142,25 +142,25 @@ const MobileMenu = () => {
|
||||
{filteredLinks.map((link) => {
|
||||
const isActive = router.pathname.match(link.activeRegExp);
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`flex items-center space-x-2 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
|
||||
className: 'h-5 w-5',
|
||||
})}
|
||||
<span>{link.content}</span>
|
||||
</a>
|
||||
<Link
|
||||
key={`mobile-menu-link-${link.href}`}
|
||||
href={link.href}
|
||||
className={`flex items-center space-x-2 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
|
||||
className: 'h-5 w-5',
|
||||
})}
|
||||
<span>{link.content}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -173,19 +173,19 @@ const MobileMenu = () => {
|
||||
const isActive =
|
||||
router.pathname.match(link.activeRegExp) && !isOpen;
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`flex flex-col items-center space-y-1 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
>
|
||||
{cloneElement(
|
||||
isActive ? link.svgIconSelected : link.svgIcon,
|
||||
{
|
||||
className: 'h-6 w-6',
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
<Link
|
||||
key={`mobile-menu-link-${link.href}`}
|
||||
href={link.href}
|
||||
className={`flex flex-col items-center space-y-1 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
>
|
||||
{cloneElement(
|
||||
isActive ? link.svgIconSelected : link.svgIcon,
|
||||
{
|
||||
className: 'h-6 w-6',
|
||||
}
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import useSearchInput from '@app/hooks/useSearchInput';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { XCircleIcon } from '@heroicons/react/24/outline';
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Layout.SearchInput', {
|
||||
searchPlaceholder: 'Search Movies & TV',
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import UserWarnings from '@app/components/Layout/UserWarnings';
|
||||
import VersionStatus from '@app/components/Layout/VersionStatus';
|
||||
import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import {
|
||||
ClockIcon,
|
||||
@@ -13,12 +14,13 @@ import {
|
||||
UsersIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Fragment, useRef } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
export const menuMessages = defineMessages({
|
||||
export const menuMessages = defineMessages('components.Layout.Sidebar', {
|
||||
dashboard: 'Discover',
|
||||
browsemovies: 'Movies',
|
||||
browsetv: 'Series',
|
||||
@@ -146,16 +148,16 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
</div>
|
||||
<div
|
||||
ref={navRef}
|
||||
className="flex flex-1 flex-col overflow-y-auto pt-8 pb-8 sm:pb-4"
|
||||
className="flex flex-1 flex-col overflow-y-auto pt-4 pb-8 sm:pb-4"
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center px-2">
|
||||
<span className="px-4 text-xl text-gray-50">
|
||||
<a href="/">
|
||||
<img src="/logo_full.svg" alt="Logo" />
|
||||
</a>
|
||||
<span className="w-full px-4 text-xl text-gray-50">
|
||||
<Link href="/" className="relative block h-24 w-64">
|
||||
<Image src="/logo_full.svg" alt="Logo" fill />
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<nav className="mt-16 flex-1 space-y-4 px-4">
|
||||
<nav className="mt-10 flex-1 space-y-4 px-4">
|
||||
{SidebarLinks.filter((link) =>
|
||||
link.requiredPermission
|
||||
? hasPermission(link.requiredPermission, {
|
||||
@@ -168,32 +170,27 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
key={`mobile-${sidebarLink.messagesKey}`}
|
||||
href={sidebarLink.href}
|
||||
as={sidebarLink.as}
|
||||
onClick={() => setClosed()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setClosed();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
|
||||
${
|
||||
router.pathname.match(sidebarLink.activeRegExp)
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
data-testid={`${sidebarLink.dataTestId}-mobile`}
|
||||
>
|
||||
<a
|
||||
onClick={() => setClosed()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setClosed();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
|
||||
${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
data-testid={`${sidebarLink.dataTestId}-mobile`}
|
||||
>
|
||||
{sidebarLink.svgIcon}
|
||||
{intl.formatMessage(
|
||||
menuMessages[sidebarLink.messagesKey]
|
||||
)}
|
||||
</a>
|
||||
{sidebarLink.svgIcon}
|
||||
{intl.formatMessage(
|
||||
menuMessages[sidebarLink.messagesKey]
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -221,15 +218,15 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="sidebar flex w-64 flex-col">
|
||||
<div className="flex h-0 flex-1 flex-col">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-8 pb-4">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pb-4">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
<span className="px-4 text-2xl text-gray-50">
|
||||
<a href="/">
|
||||
<img src="/logo_full.svg" alt="Logo" />
|
||||
</a>
|
||||
<span className="w-full px-4 py-2 text-2xl text-gray-50">
|
||||
<Link href="/" className="relative block h-24">
|
||||
<Image src="/logo_full.svg" alt="Logo" fill />
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<nav className="mt-16 flex-1 space-y-4 px-4">
|
||||
<nav className="mt-8 flex-1 space-y-4 px-4">
|
||||
{SidebarLinks.filter((link) =>
|
||||
link.requiredPermission
|
||||
? hasPermission(link.requiredPermission, {
|
||||
@@ -242,24 +239,19 @@ const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
key={`desktop-${sidebarLink.messagesKey}`}
|
||||
href={sidebarLink.href}
|
||||
as={sidebarLink.as}
|
||||
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
|
||||
${
|
||||
router.pathname.match(sidebarLink.activeRegExp)
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
data-testid={sidebarLink.dataTestId}
|
||||
>
|
||||
<a
|
||||
className={`group flex items-center rounded-md px-2 py-2 text-lg font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
|
||||
${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
data-testid={sidebarLink.dataTestId}
|
||||
>
|
||||
{sidebarLink.svgIcon}
|
||||
{intl.formatMessage(
|
||||
menuMessages[sidebarLink.messagesKey]
|
||||
)}
|
||||
</a>
|
||||
{sidebarLink.svgIcon}
|
||||
{intl.formatMessage(
|
||||
menuMessages[sidebarLink.messagesKey]
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import Infinity from '@app/assets/infinity.svg';
|
||||
import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
|
||||
import ProgressCircle from '@app/components/Common/ProgressCircle';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
movierequests: 'Movie Requests',
|
||||
seriesrequests: 'Series Requests',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Layout.UserDropdown.MiniQuotaDisplay',
|
||||
{
|
||||
movierequests: 'Movie Requests',
|
||||
seriesrequests: 'Series Requests',
|
||||
}
|
||||
);
|
||||
|
||||
type MiniQuotaDisplayProps = {
|
||||
userId: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import {
|
||||
ArrowRightOnRectangleIcon,
|
||||
@@ -7,12 +8,13 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import type { LinkProps } from 'next/link';
|
||||
import Link from 'next/link';
|
||||
import { forwardRef, Fragment } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Layout.UserDropdown', {
|
||||
myprofile: 'Profile',
|
||||
settings: 'Settings',
|
||||
requests: 'Requests',
|
||||
@@ -24,10 +26,8 @@ const ForwardedLink = forwardRef<
|
||||
LinkProps & React.ComponentPropsWithoutRef<'a'>
|
||||
>(({ href, children, ...rest }, ref) => {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a ref={ref} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
<Link href={href} ref={ref} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
@@ -53,10 +53,12 @@ const UserDropdown = () => {
|
||||
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
|
||||
data-testid="user-menu"
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||
src={user?.avatar}
|
||||
src={user?.avatar || ''}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
@@ -74,10 +76,12 @@ const UserDropdown = () => {
|
||||
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
|
||||
<div className="flex flex-col space-y-4 px-4 py-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<img
|
||||
<Image
|
||||
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
|
||||
src={user?.avatar}
|
||||
src={user?.avatar || ''}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate text-xl font-semibold text-gray-200">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
import Link from 'next/link';
|
||||
import type React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Layout.UserWarnings', {
|
||||
emailRequired: 'An email address is required.',
|
||||
emailInvalid: 'Email address is invalid.',
|
||||
passwordRequired: 'A password is required.',
|
||||
@@ -37,24 +37,23 @@ const UserWarnings: React.FC<UserWarningsProps> = ({ onClick }) => {
|
||||
}
|
||||
|
||||
res = (
|
||||
<Link href={link}>
|
||||
<a
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
|
||||
>
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||
<span className="font-bold">{warningTitle}</span>
|
||||
<span className="truncate">{warningText}</span>
|
||||
</div>
|
||||
</a>
|
||||
<Link
|
||||
href={link}
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="mx-2 mb-2 flex items-center rounded-lg bg-yellow-500 p-2 text-xs text-white ring-1 ring-gray-700 transition duration-300 hover:bg-yellow-400"
|
||||
>
|
||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||
<span className="font-bold">{warningTitle}</span>
|
||||
<span className="truncate">{warningText}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ArrowUpCircleIcon,
|
||||
BeakerIcon,
|
||||
@@ -6,10 +7,10 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Layout.VersionStatus', {
|
||||
streamdevelop: 'Jellyseerr Develop',
|
||||
streamstable: 'Jellyseerr Stable',
|
||||
outofdate: 'Out of Date',
|
||||
@@ -39,49 +40,48 @@ const VersionStatus = ({ onClick }: VersionStatusProps) => {
|
||||
: intl.formatMessage(messages.streamstable);
|
||||
|
||||
return (
|
||||
<Link href="/settings/about">
|
||||
<a
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`mx-2 flex items-center rounded-lg p-2 text-xs ring-1 ring-gray-700 transition duration-300 ${
|
||||
data.updateAvailable
|
||||
? 'bg-yellow-500 text-white hover:bg-yellow-400'
|
||||
: 'bg-gray-900 text-gray-300 hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{data.commitTag === 'local' ? (
|
||||
<CodeBracketIcon className="h-6 w-6" />
|
||||
) : data.version.startsWith('develop-') ? (
|
||||
<BeakerIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<ServerIcon className="h-6 w-6" />
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||
<span className="font-bold">{versionStream}</span>
|
||||
<span className="truncate">
|
||||
{data.commitTag === 'local' ? (
|
||||
'(⌐■_■)'
|
||||
) : data.commitsBehind > 0 ? (
|
||||
intl.formatMessage(messages.commitsbehind, {
|
||||
commitsBehind: data.commitsBehind,
|
||||
})
|
||||
) : data.commitsBehind === -1 ? (
|
||||
intl.formatMessage(messages.outofdate)
|
||||
) : (
|
||||
<code className="bg-transparent p-0">
|
||||
{data.version.replace('develop-', '')}
|
||||
</code>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{data.updateAvailable && <ArrowUpCircleIcon className="h-6 w-6" />}
|
||||
</a>
|
||||
<Link
|
||||
href="/settings/about"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onClick) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`mx-2 flex items-center rounded-lg p-2 text-xs ring-1 ring-gray-700 transition duration-300 ${
|
||||
data.updateAvailable
|
||||
? 'bg-yellow-500 text-white hover:bg-yellow-400'
|
||||
: 'bg-gray-900 text-gray-300 hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{data.commitTag === 'local' ? (
|
||||
<CodeBracketIcon className="h-6 w-6" />
|
||||
) : data.version.startsWith('develop-') ? (
|
||||
<BeakerIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<ServerIcon className="h-6 w-6" />
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-col truncate px-2 last:pr-0">
|
||||
<span className="font-bold">{versionStream}</span>
|
||||
<span className="truncate">
|
||||
{data.commitTag === 'local' ? (
|
||||
'(⌐■_■)'
|
||||
) : data.commitsBehind > 0 ? (
|
||||
intl.formatMessage(messages.commitsbehind, {
|
||||
commitsBehind: data.commitsBehind,
|
||||
})
|
||||
) : data.commitsBehind === -1 ? (
|
||||
intl.formatMessage(messages.outofdate)
|
||||
) : (
|
||||
<code className="bg-transparent p-0">
|
||||
{data.version.replace('develop-', '')}
|
||||
</code>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{data.updateAvailable && <ArrowUpCircleIcon className="h-6 w-6" />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
import type React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Login', {
|
||||
title: 'Add Email',
|
||||
description:
|
||||
'Since this is your first time logging into {applicationName}, you are required to add a valid email address.',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
import type React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Login', {
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
hostname: '{mediaServerName} URL',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ArrowLeftOnRectangleIcon,
|
||||
LifebuoyIcon,
|
||||
@@ -9,10 +10,10 @@ import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Login', {
|
||||
username: 'Username',
|
||||
email: 'Email Address',
|
||||
password: 'Password',
|
||||
@@ -137,7 +138,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
|
||||
</span>
|
||||
{passwordResetEnabled && (
|
||||
<span className="inline-flex rounded-md shadow-sm">
|
||||
<Link href="/resetpassword" passHref>
|
||||
<Link href="/resetpassword" passHref legacyBehavior>
|
||||
<Button as="a" buttonType="ghost">
|
||||
<LifebuoyIcon />
|
||||
<span>
|
||||
|
||||
@@ -6,18 +6,20 @@ import LocalLogin from '@app/components/Login/LocalLogin';
|
||||
import PlexLoginButton from '@app/components/PlexLoginButton';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { XCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import axios from 'axios';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/dist/client/router';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import JellyfinLogin from './JellyfinLogin';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Login', {
|
||||
signin: 'Sign In',
|
||||
signinheader: 'Sign in to continue',
|
||||
signinwithplex: 'Use your Plex account',
|
||||
@@ -86,8 +88,10 @@ const Login = () => {
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<div className="relative z-40 mt-10 flex flex-col items-center px-4 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<img src="/logo_stacked.svg" className="mb-10 max-w-full" alt="Logo" />
|
||||
<h2 className="mt-2 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
<div className="relative h-48 w-full max-w-full">
|
||||
<Image src="/logo_stacked.svg" alt="Logo" fill />
|
||||
</div>
|
||||
<h2 className="mt-12 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
{intl.formatMessage(messages.signinheader)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import RequestBlock from '@app/components/RequestBlock';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Bars4Icon, ServerIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
@@ -27,11 +28,12 @@ import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import getConfig from 'next/config';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.ManageSlideOver', {
|
||||
manageModalTitle: 'Manage {mediaType}',
|
||||
manageModalIssues: 'Open Issues',
|
||||
manageModalRequests: 'Requests',
|
||||
@@ -328,19 +330,20 @@ const ManageSlideOver = ({
|
||||
: `/users/${user.id}`
|
||||
}
|
||||
key={`watch-user-${user.id}`}
|
||||
className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"
|
||||
>
|
||||
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50">
|
||||
<Tooltip
|
||||
key={`watch-user-${user.id}`}
|
||||
content={user.displayName}
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.displayName}
|
||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
/>
|
||||
</Tooltip>
|
||||
</a>
|
||||
<Tooltip
|
||||
key={`watch-user-${user.id}`}
|
||||
content={user.displayName}
|
||||
>
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt={user.displayName}
|
||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
@@ -488,19 +491,20 @@ const ManageSlideOver = ({
|
||||
: `/users/${user.id}`
|
||||
}
|
||||
key={`watch-user-${user.id}`}
|
||||
className="z-0 mb-1 -mr-2 shrink-0 hover:z-50"
|
||||
>
|
||||
<a className="z-0 mb-1 -mr-2 shrink-0 hover:z-50">
|
||||
<Tooltip
|
||||
key={`watch-user-${user.id}`}
|
||||
content={user.displayName}
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.displayName}
|
||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
/>
|
||||
</Tooltip>
|
||||
</a>
|
||||
<Tooltip
|
||||
key={`watch-user-${user.id}`}
|
||||
content={user.displayName}
|
||||
>
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt={user.displayName}
|
||||
className="h-8 w-8 scale-100 transform-gpu rounded-full object-cover ring-1 ring-gray-500 transition duration-300 hover:scale-105"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
))}
|
||||
</span>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import TitleCard from '@app/components/TitleCard';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MediaSlider.ShowMoreCard', {
|
||||
seemore: 'See More',
|
||||
});
|
||||
|
||||
@@ -30,79 +32,82 @@ const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<a
|
||||
className={'w-36 sm:w-36 md:w-44'}
|
||||
onMouseEnter={() => {
|
||||
<Link
|
||||
href={url}
|
||||
className={'w-36 sm:w-36 md:w-44'}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className={`relative w-36 transform-gpu cursor-pointer
|
||||
overflow-hidden rounded-xl text-white shadow-lg ring-1 transition duration-150 ease-in-out sm:w-36 md:w-44 ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-600 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`relative w-36 transform-gpu cursor-pointer
|
||||
overflow-hidden rounded-xl text-white shadow-lg ring-1 transition duration-150 ease-in-out sm:w-36 md:w-44 ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-600 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30">
|
||||
{posters[0] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[1] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[2] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[3] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<img
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
|
||||
<ArrowRightCircleIcon className="w-14" />
|
||||
<div className="mt-2 font-extrabold">
|
||||
{intl.formatMessage(messages.seemore)}
|
||||
<div style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div className="relative z-10 flex h-full flex-wrap items-center justify-center opacity-30">
|
||||
{posters[0] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[0]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[1] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[1]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[2] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[2]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{posters[3] && (
|
||||
<div className="w-1/2 p-1">
|
||||
<Image
|
||||
src={`//image.tmdb.org/t/p/w300_and_h450_face${posters[3]}`}
|
||||
alt=""
|
||||
className="w-full rounded-md"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-white">
|
||||
<ArrowRightCircleIcon className="w-14" />
|
||||
<div className="mt-2 font-extrabold">
|
||||
{intl.formatMessage(messages.seemore)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -152,11 +152,9 @@ const MediaSlider = ({
|
||||
<>
|
||||
<div className="slider-header">
|
||||
{linkUrl ? (
|
||||
<Link href={linkUrl}>
|
||||
<a className="slider-title min-w-0 pr-16">
|
||||
<span className="truncate">{title}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link href={linkUrl} className="slider-title min-w-0 pr-16">
|
||||
<span className="truncate">{title}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
) : (
|
||||
<div className="slider-title">
|
||||
|
||||
@@ -3,13 +3,14 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MovieDetails.MovieCast', {
|
||||
fullcast: 'Full Cast',
|
||||
});
|
||||
|
||||
@@ -34,8 +35,8 @@ const MovieCast = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header
|
||||
subtext={
|
||||
<Link href={`/movie/${data.id}`}>
|
||||
<a className="hover:underline">{data.title}</a>
|
||||
<Link href={`/movie/${data.id}`} className="hover:underline">
|
||||
{data.title}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,13 +3,14 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import PersonCard from '@app/components/PersonCard';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MovieDetails.MovieCrew', {
|
||||
fullcrew: 'Full Crew',
|
||||
});
|
||||
|
||||
@@ -34,8 +35,8 @@ const MovieCrew = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header
|
||||
subtext={
|
||||
<Link href={`/movie/${data.id}`}>
|
||||
<a className="hover:underline">{data.title}</a>
|
||||
<Link href={`/movie/${data.id}`} className="hover:underline">
|
||||
{data.title}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MovieDetails', {
|
||||
recommendations: 'Recommendations',
|
||||
});
|
||||
|
||||
@@ -44,8 +45,8 @@ const MovieRecommendations = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header
|
||||
subtext={
|
||||
<Link href={`/movie/${movieData?.id}`}>
|
||||
<a className="hover:underline">{movieData?.title}</a>
|
||||
<Link href={`/movie/${movieData?.id}`} className="hover:underline">
|
||||
{movieData?.title}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,14 +3,15 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { MovieResult } from '@server/models/Search';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MovieDetails', {
|
||||
similar: 'Similar Titles',
|
||||
});
|
||||
|
||||
@@ -42,8 +43,8 @@ const MovieSimilar = () => {
|
||||
<div className="mt-1 mb-5">
|
||||
<Header
|
||||
subtext={
|
||||
<Link href={`/movie/${movieData?.id}`}>
|
||||
<a className="hover:underline">{movieData?.title}</a>
|
||||
<Link href={`/movie/${movieData?.id}`} className="hover:underline">
|
||||
{movieData?.title}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import { sortCrewPriority } from '@app/utils/creditHelpers';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@@ -46,17 +47,17 @@ import { IssueStatus } from '@server/constants/issue';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||
import { hasFlag } from 'country-flag-icons';
|
||||
import { countries } from 'country-flag-icons';
|
||||
import 'country-flag-icons/3x2/flags.css';
|
||||
import { uniqBy } from 'lodash';
|
||||
import getConfig from 'next/config';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.MovieDetails', {
|
||||
originaltitle: 'Original Title',
|
||||
releasedate:
|
||||
'{releaseCount, plural, one {Release Date} other {Release Dates}}',
|
||||
@@ -239,8 +240,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
movieAttributes.push(
|
||||
data.genres
|
||||
.map((g) => (
|
||||
<Link href={`/discover/movies?genre=${g.id}`} key={`genre-${g.id}`}>
|
||||
<a className="hover:underline">{g.name}</a>
|
||||
<Link
|
||||
href={`/discover/movies?genre=${g.id}`}
|
||||
key={`genre-${g.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{g.name}
|
||||
</Link>
|
||||
))
|
||||
.reduce((prev, curr) => (
|
||||
@@ -294,8 +299,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
priority
|
||||
/>
|
||||
<div
|
||||
@@ -336,7 +341,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
width={600}
|
||||
height={900}
|
||||
priority
|
||||
@@ -483,18 +489,19 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
{sortedCrew.slice(0, 6).map((person) => (
|
||||
<li key={`crew-${person.job}-${person.id}`}>
|
||||
<span>{person.job}</span>
|
||||
<Link href={`/person/${person.id}`}>
|
||||
<a className="crew-name">{person.name}</a>
|
||||
<Link href={`/person/${person.id}`} className="crew-name">
|
||||
{person.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Link href={`/movie/${data.id}/crew`}>
|
||||
<a className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100">
|
||||
<span>{intl.formatMessage(messages.viewfullcrew)}</span>
|
||||
<ArrowRightCircleIcon className="ml-1.5 inline-block h-5 w-5" />
|
||||
</a>
|
||||
<Link
|
||||
href={`/movie/${data.id}/crew`}
|
||||
className="flex items-center text-gray-400 transition duration-300 hover:text-gray-100"
|
||||
>
|
||||
<span>{intl.formatMessage(messages.viewfullcrew)}</span>
|
||||
<ArrowRightCircleIcon className="ml-1.5 inline-block h-5 w-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
@@ -505,10 +512,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
<Link
|
||||
href={`/discover/movies?keywords=${keyword.id}`}
|
||||
key={`keyword-id-${keyword.id}`}
|
||||
className="mb-2 mr-2 inline-flex last:mr-0"
|
||||
>
|
||||
<a className="mb-2 mr-2 inline-flex last:mr-0">
|
||||
<Tag>{keyword.name}</Tag>
|
||||
</a>
|
||||
<Tag>{keyword.name}</Tag>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -518,31 +524,33 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
{data.collection && (
|
||||
<div className="mb-6">
|
||||
<Link href={`/collection/${data.collection.id}`}>
|
||||
<a>
|
||||
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 flex h-full items-center justify-between p-4 text-gray-200 transition duration-300 group-hover:text-white">
|
||||
<div>{data.collection.name}</div>
|
||||
<Button buttonSize="sm">
|
||||
{intl.formatMessage(globalMessages.view)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="group relative z-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-lg bg-gray-800 bg-cover bg-center shadow-md ring-1 ring-gray-700 transition duration-300 hover:scale-105 hover:ring-gray-500">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1440_and_h320_multi_faces/${data.collection.backdropPath}`}
|
||||
alt=""
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(180deg, rgba(31, 41, 55, 0.47) 0%, rgba(31, 41, 55, 0.80) 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<div className="relative z-10 flex h-full items-center justify-between p-4 text-gray-200 transition duration-300 group-hover:text-white">
|
||||
<div>{data.collection.name}</div>
|
||||
<Button buttonSize="sm">
|
||||
{intl.formatMessage(globalMessages.view)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -739,15 +747,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
<Link
|
||||
href={`/discover/movies/language/${data.originalLanguage}`}
|
||||
>
|
||||
<a>
|
||||
{intl.formatDisplayName(data.originalLanguage, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ??
|
||||
data.spokenLanguages.find(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
)?.name}
|
||||
</a>
|
||||
{intl.formatDisplayName(data.originalLanguage, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ??
|
||||
data.spokenLanguages.find(
|
||||
(lng) => lng.iso_639_1 === data.originalLanguage
|
||||
)?.name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
@@ -766,7 +772,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
className="flex items-center justify-end"
|
||||
key={`prodcountry-${c.iso_3166_1}`}
|
||||
>
|
||||
{hasFlag(c.iso_3166_1) && (
|
||||
{countries.includes(c.iso_3166_1) && (
|
||||
<span
|
||||
className={`mr-1.5 text-xs leading-5 flag:${c.iso_3166_1}`}
|
||||
/>
|
||||
@@ -803,8 +809,9 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
<Link
|
||||
href={`/discover/movies/studio/${s.id}`}
|
||||
key={`studio-${s.id}`}
|
||||
className="block"
|
||||
>
|
||||
<a className="block">{s.name}</a>
|
||||
{s.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
@@ -864,11 +871,13 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
{data.credits.cast.length > 0 && (
|
||||
<>
|
||||
<div className="slider-header">
|
||||
<Link href="/movie/[movieId]/cast" as={`/movie/${data.id}/cast`}>
|
||||
<a className="slider-title">
|
||||
<span>{intl.formatMessage(messages.cast)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</a>
|
||||
<Link
|
||||
href="/movie/[movieId]/cast"
|
||||
as={`/movie/${data.id}/cast`}
|
||||
className="slider-title"
|
||||
>
|
||||
<span>{intl.formatMessage(messages.cast)}</span>
|
||||
<ArrowRightCircleIcon />
|
||||
</Link>
|
||||
</div>
|
||||
<Slider
|
||||
|
||||
@@ -2,11 +2,12 @@ import NotificationType from '@app/components/NotificationTypeSelector/Notificat
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import type { User } from '@app/hooks/useUser';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { sortBy } from 'lodash';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.NotificationTypeSelector', {
|
||||
notificationTypes: 'Notification Types',
|
||||
mediarequested: 'Request Pending Approval',
|
||||
mediarequestedDescription:
|
||||
|
||||
@@ -3,10 +3,11 @@ import PermissionOption from '@app/components/PermissionOption';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import type { User } from '@app/hooks/useUser';
|
||||
import { Permission } from '@app/hooks/useUser';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
export const messages = defineMessages({
|
||||
export const messages = defineMessages('components.PermissionEdit', {
|
||||
admin: 'Admin',
|
||||
adminDescription:
|
||||
'Full administrator access. Bypasses all other permission checks.',
|
||||
|
||||
@@ -21,71 +21,72 @@ const PersonCard = ({
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<Link href={`/person/${personId}`}>
|
||||
<a
|
||||
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
|
||||
onMouseEnter={() => {
|
||||
<Link
|
||||
href={`/person/${personId}`}
|
||||
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setHovered(true);
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
}
|
||||
}}
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className={`relative ${
|
||||
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
|
||||
} transform-gpu cursor-pointer rounded-xl text-white shadow ring-1 transition duration-150 ease-in-out ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`relative ${
|
||||
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
|
||||
} transform-gpu cursor-pointer rounded-xl text-white shadow ring-1 transition duration-150 ease-in-out ${
|
||||
isHovered
|
||||
? 'scale-105 bg-gray-700 ring-gray-500'
|
||||
: 'scale-100 bg-gray-800 ring-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div className="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
{profilePath ? (
|
||||
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<UserCircleIcon className="h-full" />
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full truncate text-center font-bold">
|
||||
{name}
|
||||
</div>
|
||||
{subName && (
|
||||
<div
|
||||
className="overflow-hidden whitespace-normal text-center text-sm text-gray-300"
|
||||
style={{
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{subName}
|
||||
<div style={{ paddingBottom: '150%' }}>
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||
<div className="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||
{profilePath ? (
|
||||
<div className="relative h-full w-3/4 overflow-hidden rounded-full ring-1 ring-gray-700">
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath}`}
|
||||
alt=""
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<UserCircleIcon className="h-full" />
|
||||
)}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full truncate text-center font-bold">{name}</div>
|
||||
{subName && (
|
||||
<div
|
||||
className="overflow-hidden whitespace-normal text-center text-sm text-gray-300"
|
||||
style={{
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{subName}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,16 +6,17 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import TitleCard from '@app/components/TitleCard';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { PersonCombinedCreditsResponse } from '@server/interfaces/api/personInterfaces';
|
||||
import type { PersonDetails as PersonDetailsType } from '@server/models/Person';
|
||||
import { groupBy } from 'lodash';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import TruncateMarkup from 'react-truncate-markup';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.PersonDetails', {
|
||||
birthdate: 'Born {birthdate}',
|
||||
lifespan: '{birthdate} – {deathdate}',
|
||||
alsoknownas: 'Also Known As: {names}',
|
||||
@@ -228,8 +229,8 @@ const PersonDetails = () => {
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import PlexOAuth from '@app/utils/plex';
|
||||
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.PlexLoginButton', {
|
||||
signinwithplex: 'Sign In',
|
||||
signingin: 'Signing In…',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.QuotaSelector', {
|
||||
movieRequests:
|
||||
'{quotaLimit} <quotaUnits>{movies} per {quotaDays} {days}</quotaUnits>',
|
||||
tvRequests:
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
import type { Region } from '@server/lib/settings';
|
||||
import { hasFlag } from 'country-flag-icons';
|
||||
import { countries } from 'country-flag-icons';
|
||||
import 'country-flag-icons/3x2/flags.css';
|
||||
import { sortBy } from 'lodash';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RegionSelector', {
|
||||
regionDefault: 'All Regions',
|
||||
regionServerDefault: 'Default ({region})',
|
||||
});
|
||||
@@ -92,11 +93,12 @@ const RegionSelector = ({
|
||||
<div className="relative">
|
||||
<span className="inline-block w-full rounded-md shadow-sm">
|
||||
<Listbox.Button className="focus:shadow-outline-blue relative flex w-full cursor-default items-center rounded-md border border-gray-500 bg-gray-700 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
|
||||
{((selectedRegion && hasFlag(selectedRegion?.iso_3166_1)) ||
|
||||
{((selectedRegion &&
|
||||
countries.includes(selectedRegion?.iso_3166_1)) ||
|
||||
(isUserSetting &&
|
||||
!selectedRegion &&
|
||||
currentSettings.region &&
|
||||
hasFlag(currentSettings.region))) && (
|
||||
countries.includes(currentSettings.region))) && (
|
||||
<span className="mr-2 h-4 overflow-hidden text-base leading-4">
|
||||
<span
|
||||
className={`flag:${
|
||||
@@ -146,7 +148,7 @@ const RegionSelector = ({
|
||||
<span className="mr-2 text-base">
|
||||
<span
|
||||
className={
|
||||
hasFlag(currentSettings.region)
|
||||
countries.includes(currentSettings.region)
|
||||
? `flag:${currentSettings.region}`
|
||||
: 'pr-6'
|
||||
}
|
||||
@@ -215,7 +217,7 @@ const RegionSelector = ({
|
||||
<span className="mr-2 text-base">
|
||||
<span
|
||||
className={
|
||||
hasFlag(region.iso_3166_1)
|
||||
countries.includes(region.iso_3166_1)
|
||||
? `flag:${region.iso_3166_1}`
|
||||
: 'pr-6'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import RequestModal from '@app/components/RequestModal';
|
||||
import useRequestOverride from '@app/hooks/useRequestOverride';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
CalendarIcon,
|
||||
CheckIcon,
|
||||
@@ -19,9 +20,9 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestBlock', {
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
requestoverrides: 'Request Overrides',
|
||||
server: 'Destination Server',
|
||||
@@ -101,10 +102,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
? '/profile'
|
||||
: `/users/${request.requestedBy.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{request.requestedBy.displayName}
|
||||
</a>
|
||||
{request.requestedBy.displayName}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
@@ -120,10 +120,9 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
? '/profile'
|
||||
: `/users/${request.modifiedBy.id}`
|
||||
}
|
||||
className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline"
|
||||
>
|
||||
<a className="font-semibold text-gray-100 transition duration-300 hover:text-white hover:underline">
|
||||
{request.modifiedBy.displayName}
|
||||
</a>
|
||||
{request.modifiedBy.displayName}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import RequestModal from '@app/components/RequestModal';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -14,9 +15,9 @@ import type Media from '@server/entity/Media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import axios from 'axios';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestButton', {
|
||||
viewrequest: 'View Request',
|
||||
viewrequest4k: 'View 4K Request',
|
||||
requestmore: 'Request More',
|
||||
|
||||
@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import { withProperties } from '@app/utils/typeHelpers';
|
||||
import {
|
||||
@@ -21,14 +22,15 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestCard', {
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
failedretry: 'Something went wrong while retrying the request.',
|
||||
mediaerror: '{mediaType} Not Found',
|
||||
@@ -106,17 +108,22 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
{ type: 'or' }
|
||||
) && (
|
||||
<div className="card-field !hidden sm:!block">
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center">
|
||||
<img
|
||||
<Link
|
||||
href={`/users/${requestData.requestedBy.id}`}
|
||||
className="group flex items-center"
|
||||
>
|
||||
<span className="avatar-sm">
|
||||
<Image
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm"
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -324,8 +331,8 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
<CachedImage
|
||||
alt=""
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -352,27 +359,31 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg"
|
||||
>
|
||||
<a className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white hover:underline sm:text-lg">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) && (
|
||||
<div className="card-field">
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center">
|
||||
<img
|
||||
<Link
|
||||
href={`/users/${requestData.requestedBy.id}`}
|
||||
className="group flex items-center"
|
||||
>
|
||||
<span className="avatar-sm">
|
||||
<Image
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -572,20 +583,20 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28"
|
||||
>
|
||||
<a className="w-20 flex-shrink-0 scale-100 transform-gpu cursor-pointer overflow-hidden rounded-md shadow-sm transition duration-300 hover:scale-105 hover:shadow-md sm:w-28">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</a>
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,6 +7,7 @@ import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { refreshIntervalHelper } from '@app/utils/refreshIntervalHelper';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
@@ -20,14 +21,15 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestList.RequestItem', {
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
failedretry: 'Something went wrong while retrying the request.',
|
||||
requested: 'Requested',
|
||||
@@ -179,17 +181,22 @@ const RequestItemError = ({
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
<Link
|
||||
href={`/users/${requestData.requestedBy.id}`}
|
||||
className="group flex items-center truncate"
|
||||
>
|
||||
<span className="avatar-sm ml-1.5">
|
||||
<Image
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate text-sm group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate text-sm group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
@@ -233,17 +240,22 @@ const RequestItemError = ({
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.modifiedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
<Link
|
||||
href={`/users/${requestData.modifiedBy.id}`}
|
||||
className="group flex items-center truncate"
|
||||
>
|
||||
<span className="avatar-sm ml-1.5">
|
||||
<Image
|
||||
src={requestData.modifiedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5"
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate text-sm group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate text-sm group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
@@ -381,8 +393,8 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${title.backdropPath}`}
|
||||
alt=""
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
fill
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
@@ -401,21 +413,20 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105"
|
||||
>
|
||||
<a className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</a>
|
||||
<CachedImage
|
||||
src={
|
||||
title.posterPath
|
||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
sizes="100vw"
|
||||
style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
|
||||
width={600}
|
||||
height={900}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
|
||||
@@ -430,10 +441,9 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
? `/movie/${requestData.media.tmdbId}`
|
||||
: `/tv/${requestData.media.tmdbId}`
|
||||
}
|
||||
className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl"
|
||||
>
|
||||
<a className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</a>
|
||||
{isMovie(title) ? title.title : title.name}
|
||||
</Link>
|
||||
{!isMovie(title) && request.seasons.length > 0 && (
|
||||
<div className="card-field">
|
||||
@@ -527,17 +537,22 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.requestedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
<Link
|
||||
href={`/users/${requestData.requestedBy.id}`}
|
||||
className="group flex items-center truncate"
|
||||
>
|
||||
<span className="avatar-sm ml-1.5">
|
||||
<Image
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5 object-cover"
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.requestedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
@@ -581,17 +596,22 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
/>
|
||||
),
|
||||
user: (
|
||||
<Link href={`/users/${requestData.modifiedBy.id}`}>
|
||||
<a className="group flex items-center truncate">
|
||||
<img
|
||||
src={requestData.modifiedBy.avatar}
|
||||
<Link
|
||||
href={`/users/${requestData.modifiedBy.id}`}
|
||||
className="group flex items-center truncate"
|
||||
>
|
||||
<span className="avatar-sm ml-1.5">
|
||||
<Image
|
||||
src={requestData.requestedBy.avatar}
|
||||
alt=""
|
||||
className="avatar-sm ml-1.5 object-cover"
|
||||
className="avatar-sm object-cover"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span className="truncate text-sm font-semibold group-hover:text-white group-hover:underline">
|
||||
{requestData.modifiedBy.displayName}
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
|
||||
@@ -6,6 +6,7 @@ import RequestItem from '@app/components/RequestList/RequestItem';
|
||||
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
BarsArrowDownIcon,
|
||||
ChevronLeftIcon,
|
||||
@@ -16,10 +17,10 @@ import type { RequestResultsResponse } from '@server/interfaces/api/requestInter
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestList', {
|
||||
requests: 'Requests',
|
||||
showallrequests: 'Show All Requests',
|
||||
sortAdded: 'Most Recent',
|
||||
@@ -122,12 +123,12 @@ const RequestList = () => {
|
||||
<Header
|
||||
subtext={
|
||||
router.pathname.startsWith('/profile') ? (
|
||||
<Link href={`/profile`}>
|
||||
<a className="hover:underline">{currentUser?.displayName}</a>
|
||||
<Link href={`/profile`} className="hover:underline">
|
||||
{currentUser?.displayName}
|
||||
</Link>
|
||||
) : router.query.userId ? (
|
||||
<Link href={`/users/${user?.id}`}>
|
||||
<a className="hover:underline">{user?.displayName}</a>
|
||||
<Link href={`/users/${user?.id}`} className="hover:underline">
|
||||
{user?.displayName}
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SmallLoadingSpinner } from '@app/components/Common/LoadingSpinner';
|
||||
import type { User } from '@app/hooks/useUser';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { formatBytes } from '@app/utils/numberHelpers';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||
@@ -13,8 +14,9 @@ import type {
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import { hasPermission } from '@server/lib/permissions';
|
||||
import { isEqual } from 'lodash';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import Select from 'react-select';
|
||||
import useSWR from 'swr';
|
||||
|
||||
@@ -23,7 +25,7 @@ type OptionType = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal.AdvancedRequester', {
|
||||
advancedoptions: 'Advanced',
|
||||
destinationserver: 'Destination Server',
|
||||
qualityprofile: 'Quality Profile',
|
||||
@@ -559,10 +561,12 @@ const AdvancedRequester = ({
|
||||
<span className="inline-block w-full rounded-md shadow-sm">
|
||||
<Listbox.Button className="focus:shadow-outline-blue relative w-full cursor-default rounded-md border border-gray-700 bg-gray-800 py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5">
|
||||
<span className="flex items-center">
|
||||
<img
|
||||
<Image
|
||||
src={selectedUser.avatar}
|
||||
alt=""
|
||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className="ml-3 block">
|
||||
{selectedUser.displayName}
|
||||
@@ -609,10 +613,12 @@ const AdvancedRequester = ({
|
||||
selected ? 'font-semibold' : 'font-normal'
|
||||
} flex items-center`}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt=""
|
||||
className="h-6 w-6 flex-shrink-0 rounded-full object-cover"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className="ml-3 block flex-shrink-0">
|
||||
{user.displayName}
|
||||
|
||||
@@ -7,6 +7,7 @@ import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester';
|
||||
import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||
@@ -14,11 +15,11 @@ import { Permission } from '@server/lib/permissions';
|
||||
import type { Collection } from '@server/models/Collection';
|
||||
import axios from 'axios';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal', {
|
||||
requestadmin: 'This request will be approved automatically.',
|
||||
requestSuccess: '<strong>{title}</strong> requested successfully!',
|
||||
requestcollectiontitle: 'Request Collection',
|
||||
@@ -402,10 +403,14 @@ const CollectionRequestModal = ({
|
||||
: '/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
sizes="100vw"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
width={600}
|
||||
height={900}
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center pl-2">
|
||||
|
||||
@@ -5,6 +5,7 @@ import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester';
|
||||
import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||
@@ -12,11 +13,11 @@ import { Permission } from '@server/lib/permissions';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import axios from 'axios';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal', {
|
||||
requestadmin: 'This request will be approved automatically.',
|
||||
requestSuccess: '<strong>{title}</strong> requested successfully!',
|
||||
requestCancel: 'Request for <strong>{title}</strong> canceled.',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import ProgressCircle from '@app/components/Common/ProgressCircle';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid';
|
||||
import type { QuotaStatus } from '@server/interfaces/api/userInterfaces';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal.QuotaDisplay', {
|
||||
requestsremaining:
|
||||
'{remaining, plural, =0 {No} other {<strong>#</strong>}} {type} {remaining, plural, one {request} other {requests}} remaining',
|
||||
movielimit: '{limit, plural, one {movie} other {movies}}',
|
||||
@@ -131,10 +132,9 @@ const QuotaDisplay = ({
|
||||
ProfileLink: (msg: React.ReactNode) => (
|
||||
<Link
|
||||
href={userOverride ? `/users/${userOverride}` : '/profile'}
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
>
|
||||
<a className="text-white transition duration-300 hover:underline">
|
||||
{msg}
|
||||
</a>
|
||||
{msg}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import Alert from '@app/components/Common/Alert';
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type { SonarrSeries } from '@server/api/servarr/sonarr';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Image from 'next/image';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal.SearchByNameModal', {
|
||||
notvdbiddescription:
|
||||
'We were unable to automatically match this series. Please select the correct match below.',
|
||||
nomatches: 'We were unable to find a match for this series.',
|
||||
@@ -87,13 +89,14 @@ const SearchByNameModal = ({
|
||||
} `}
|
||||
>
|
||||
<div className="flex w-24 flex-none items-center space-x-4">
|
||||
<img
|
||||
<Image
|
||||
src={
|
||||
item.remotePoster ??
|
||||
'/images/overseerr_poster_not_found.png'
|
||||
}
|
||||
alt={item.title}
|
||||
className="h-100 w-auto rounded-md"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow self-start p-3 text-left">
|
||||
|
||||
@@ -8,6 +8,7 @@ import SearchByNameModal from '@app/components/RequestModal/SearchByNameModal';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
@@ -17,11 +18,11 @@ import { Permission } from '@server/lib/permissions';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.RequestModal', {
|
||||
requestadmin: 'This request will be approved automatically.',
|
||||
requestSuccess: '<strong>{title}</strong> requested successfully!',
|
||||
requestseriestitle: 'Request Series',
|
||||
|
||||
@@ -2,15 +2,17 @@ import Button from '@app/components/Common/Button';
|
||||
import ImageFader from '@app/components/Common/ImageFader';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import LanguagePicker from '@app/components/Layout/LanguagePicker';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowLeftIcon, EnvelopeIcon } from '@heroicons/react/24/solid';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.ResetPassword', {
|
||||
passwordreset: 'Password Reset',
|
||||
resetpassword: 'Reset your password',
|
||||
emailresetlink: 'Email Recovery Link',
|
||||
@@ -49,8 +51,10 @@ const ResetPassword = () => {
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<div className="relative z-40 mt-10 flex flex-col items-center px-4 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<img src="/logo_stacked.svg" className="mb-10 max-w-full" alt="Logo" />
|
||||
<h2 className="mt-2 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
<div className="relative h-48 w-full max-w-full">
|
||||
<Image src="/logo_stacked.svg" alt="Logo" fill />
|
||||
</div>
|
||||
<h2 className="mt-12 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
{intl.formatMessage(messages.resetpassword)}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -66,7 +70,7 @@ const ResetPassword = () => {
|
||||
{intl.formatMessage(messages.requestresetlinksuccessmessage)}
|
||||
</p>
|
||||
<span className="mt-4 flex justify-center rounded-md shadow-sm">
|
||||
<Link href="/login" passHref>
|
||||
<Link href="/login" passHref legacyBehavior>
|
||||
<Button as="a" buttonType="ghost">
|
||||
<ArrowLeftIcon />
|
||||
<span>{intl.formatMessage(messages.gobacklogin)}</span>
|
||||
|
||||
@@ -3,16 +3,18 @@ import ImageFader from '@app/components/Common/ImageFader';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import LanguagePicker from '@app/components/Layout/LanguagePicker';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { LifebuoyIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.ResetPassword', {
|
||||
passwordreset: 'Password Reset',
|
||||
resetpassword: 'Reset your password',
|
||||
password: 'Password',
|
||||
@@ -64,8 +66,10 @@ const ResetPassword = () => {
|
||||
<LanguagePicker />
|
||||
</div>
|
||||
<div className="relative z-40 mt-10 flex flex-col items-center px-4 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<img src="/logo_stacked.svg" className="mb-10 max-w-full" alt="Logo" />
|
||||
<h2 className="mt-2 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
<div className="relative h-48 w-full max-w-full">
|
||||
<Image src="/logo_stacked.svg" alt="Logo" fill />
|
||||
</div>
|
||||
<h2 className="mt-12 text-center text-3xl font-extrabold leading-9 text-gray-100">
|
||||
{intl.formatMessage(messages.resetpassword)}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -81,7 +85,7 @@ const ResetPassword = () => {
|
||||
{intl.formatMessage(messages.resetpasswordsuccessmessage)}
|
||||
</p>
|
||||
<span className="mt-4 flex justify-center rounded-md shadow-sm">
|
||||
<Link href="/login" passHref>
|
||||
<Link href="/login" passHref legacyBehavior>
|
||||
<Button as="a" buttonType="ghost">
|
||||
{intl.formatMessage(messages.gobacklogin)}
|
||||
</Button>
|
||||
|
||||
@@ -3,15 +3,16 @@ import ListView from '@app/components/Common/ListView';
|
||||
import PageTitle from '@app/components/Common/PageTitle';
|
||||
import useDiscover from '@app/hooks/useDiscover';
|
||||
import Error from '@app/pages/_error';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import type {
|
||||
MovieResult,
|
||||
PersonResult,
|
||||
TvResult,
|
||||
} from '@server/models/Search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Search', {
|
||||
search: 'Search',
|
||||
searchresults: 'Search Results',
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RegionSelector from '@app/components/RegionSelector';
|
||||
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/20/solid';
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid';
|
||||
import type {
|
||||
@@ -20,12 +21,12 @@ import type {
|
||||
import axios from 'axios';
|
||||
import { orderBy } from 'lodash';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import type { MultiValue, SingleValue } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Selector', {
|
||||
searchKeywords: 'Search keywords…',
|
||||
searchGenres: 'Select genres…',
|
||||
searchStudios: 'Search studios…',
|
||||
@@ -376,9 +377,12 @@ export const WatchProviderSelector = ({
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
fill
|
||||
className="rounded-lg"
|
||||
/>
|
||||
{isActive && (
|
||||
@@ -418,9 +422,12 @@ export const WatchProviderSelector = ({
|
||||
<CachedImage
|
||||
src={`https://image.tmdb.org/t/p/original${provider.logoPath}`}
|
||||
alt=""
|
||||
layout="responsive"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
fill
|
||||
className="rounded-lg"
|
||||
/>
|
||||
{isActive && (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/solid';
|
||||
import { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useClipboard from 'react-use-clipboard';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Settings', {
|
||||
copied: 'Copied API key to clipboard.',
|
||||
});
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Settings.Notifications', {
|
||||
agentenabled: 'Enable Agent',
|
||||
botUsername: 'Bot Username',
|
||||
botAvatarUrl: 'Bot Avatar URL',
|
||||
|
||||
@@ -3,16 +3,17 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import SettingsBadge from '@app/components/Settings/SettingsBadge';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Settings.Notifications', {
|
||||
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
|
||||
validationSmtpPortRequired: 'You must provide a valid port number',
|
||||
agentenabled: 'Enable Agent',
|
||||
|
||||
@@ -2,29 +2,33 @@ import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/solid';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
url: 'Server URL',
|
||||
token: 'Application Token',
|
||||
validationUrlRequired: 'You must provide a valid URL',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationTokenRequired: 'You must provide an application token',
|
||||
gotifysettingssaved: 'Gotify notification settings saved successfully!',
|
||||
gotifysettingsfailed: 'Gotify notification settings failed to save.',
|
||||
toastGotifyTestSending: 'Sending Gotify test notification…',
|
||||
toastGotifyTestSuccess: 'Gotify test notification sent!',
|
||||
toastGotifyTestFailed: 'Gotify test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsGotify',
|
||||
{
|
||||
agentenabled: 'Enable Agent',
|
||||
url: 'Server URL',
|
||||
token: 'Application Token',
|
||||
validationUrlRequired: 'You must provide a valid URL',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationTokenRequired: 'You must provide an application token',
|
||||
gotifysettingssaved: 'Gotify notification settings saved successfully!',
|
||||
gotifysettingsfailed: 'Gotify notification settings failed to save.',
|
||||
toastGotifyTestSending: 'Sending Gotify test notification…',
|
||||
toastGotifyTestSuccess: 'Gotify test notification sent!',
|
||||
toastGotifyTestFailed: 'Gotify test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsGotify = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -2,30 +2,35 @@ import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
webhookUrl: 'Webhook URL',
|
||||
webhookUrlTip:
|
||||
'Your user- or device-based <LunaSeaLink>notification webhook URL</LunaSeaLink>',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
profileName: 'Profile Name',
|
||||
profileNameTip: 'Only required if not using the <code>default</code> profile',
|
||||
settingsSaved: 'LunaSea notification settings saved successfully!',
|
||||
settingsFailed: 'LunaSea notification settings failed to save.',
|
||||
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
|
||||
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
|
||||
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsLunaSea',
|
||||
{
|
||||
agentenabled: 'Enable Agent',
|
||||
webhookUrl: 'Webhook URL',
|
||||
webhookUrlTip:
|
||||
'Your user- or device-based <LunaSeaLink>notification webhook URL</LunaSeaLink>',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
profileName: 'Profile Name',
|
||||
profileNameTip:
|
||||
'Only required if not using the <code>default</code> profile',
|
||||
settingsSaved: 'LunaSea notification settings saved successfully!',
|
||||
settingsFailed: 'LunaSea notification settings failed to save.',
|
||||
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
|
||||
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
|
||||
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsLunaSea = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -3,30 +3,35 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentEnabled: 'Enable Agent',
|
||||
accessToken: 'Access Token',
|
||||
accessTokenTip:
|
||||
'Create a token from your <PushbulletSettingsLink>Account Settings</PushbulletSettingsLink>',
|
||||
validationAccessTokenRequired: 'You must provide an access token',
|
||||
channelTag: 'Channel Tag',
|
||||
pushbulletSettingsSaved:
|
||||
'Pushbullet notification settings saved successfully!',
|
||||
pushbulletSettingsFailed: 'Pushbullet notification settings failed to save.',
|
||||
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
|
||||
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
|
||||
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsPushbullet',
|
||||
{
|
||||
agentEnabled: 'Enable Agent',
|
||||
accessToken: 'Access Token',
|
||||
accessTokenTip:
|
||||
'Create a token from your <PushbulletSettingsLink>Account Settings</PushbulletSettingsLink>',
|
||||
validationAccessTokenRequired: 'You must provide an access token',
|
||||
channelTag: 'Channel Tag',
|
||||
pushbulletSettingsSaved:
|
||||
'Pushbullet notification settings saved successfully!',
|
||||
pushbulletSettingsFailed:
|
||||
'Pushbullet notification settings failed to save.',
|
||||
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
|
||||
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
|
||||
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsPushbullet = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -2,35 +2,39 @@ import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import type { PushoverSound } from '@server/api/pushover';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
accessToken: 'Application API Token',
|
||||
accessTokenTip:
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr',
|
||||
userToken: 'User or Group Key',
|
||||
userTokenTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
sound: 'Notification Sound',
|
||||
deviceDefault: 'Device Default',
|
||||
validationAccessTokenRequired: 'You must provide a valid application token',
|
||||
validationUserTokenRequired: 'You must provide a valid user or group key',
|
||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||
toastPushoverTestSending: 'Sending Pushover test notification…',
|
||||
toastPushoverTestSuccess: 'Pushover test notification sent!',
|
||||
toastPushoverTestFailed: 'Pushover test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsPushover',
|
||||
{
|
||||
agentenabled: 'Enable Agent',
|
||||
accessToken: 'Application API Token',
|
||||
accessTokenTip:
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Jellyseerr',
|
||||
userToken: 'User or Group Key',
|
||||
userTokenTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
sound: 'Notification Sound',
|
||||
deviceDefault: 'Device Default',
|
||||
validationAccessTokenRequired: 'You must provide a valid application token',
|
||||
validationUserTokenRequired: 'You must provide a valid user or group key',
|
||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||
toastPushoverTestSending: 'Sending Pushover test notification…',
|
||||
toastPushoverTestSuccess: 'Pushover test notification sent!',
|
||||
toastPushoverTestFailed: 'Pushover test notification failed to send.',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsPushover = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -2,28 +2,32 @@ import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
webhookUrl: 'Webhook URL',
|
||||
webhookUrlTip:
|
||||
'Create an <WebhookLink>Incoming Webhook</WebhookLink> integration',
|
||||
slacksettingssaved: 'Slack notification settings saved successfully!',
|
||||
slacksettingsfailed: 'Slack notification settings failed to save.',
|
||||
toastSlackTestSending: 'Sending Slack test notification…',
|
||||
toastSlackTestSuccess: 'Slack test notification sent!',
|
||||
toastSlackTestFailed: 'Slack test notification failed to send.',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsSlack',
|
||||
{
|
||||
agentenabled: 'Enable Agent',
|
||||
webhookUrl: 'Webhook URL',
|
||||
webhookUrlTip:
|
||||
'Create an <WebhookLink>Incoming Webhook</WebhookLink> integration',
|
||||
slacksettingssaved: 'Slack notification settings saved successfully!',
|
||||
slacksettingsfailed: 'Slack notification settings failed to save.',
|
||||
toastSlackTestSending: 'Sending Slack test notification…',
|
||||
toastSlackTestSuccess: 'Slack test notification sent!',
|
||||
toastSlackTestFailed: 'Slack test notification failed to send.',
|
||||
validationWebhookUrl: 'You must provide a valid URL',
|
||||
validationTypes: 'You must select at least one notification type',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsSlack = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -3,16 +3,17 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
const messages = defineMessages('components.Settings.Notifications', {
|
||||
agentenabled: 'Enable Agent',
|
||||
botUsername: 'Bot Username',
|
||||
botUsernameTip:
|
||||
|
||||
@@ -2,24 +2,28 @@ import Alert from '@app/components/Common/Alert';
|
||||
import Button from '@app/components/Common/Button';
|
||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
agentenabled: 'Enable Agent',
|
||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||
toastWebPushTestSending: 'Sending web push test notification…',
|
||||
toastWebPushTestSuccess: 'Web push test notification sent!',
|
||||
toastWebPushTestFailed: 'Web push test notification failed to send.',
|
||||
httpsRequirement:
|
||||
'In order to receive web push notifications, Jellyseerr must be served over HTTPS.',
|
||||
});
|
||||
const messages = defineMessages(
|
||||
'components.Settings.Notifications.NotificationsWebPush',
|
||||
{
|
||||
agentenabled: 'Enable Agent',
|
||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||
toastWebPushTestSending: 'Sending web push test notification…',
|
||||
toastWebPushTestSuccess: 'Web push test notification sent!',
|
||||
toastWebPushTestFailed: 'Web push test notification failed to send.',
|
||||
httpsRequirement:
|
||||
'In order to receive web push notifications, Jellyseerr must be served over HTTPS.',
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationsWebPush = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user