mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: upcoming/trending list views and larger title cards
This commit is contained in:
21
src/components/Common/Header/index.tsx
Normal file
21
src/components/Common/Header/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
extraMargin?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ children, extraMargin = 0 }) => {
|
||||||
|
return (
|
||||||
|
<div className="md:flex md:items-center md:justify-between mt-8 mb-8">
|
||||||
|
<div className={`flex-1 min-w-0 mx-${extraMargin}`}>
|
||||||
|
<h2 className="text-2xl font-bold leading-7 text-cool-gray-100 sm:text-4xl sm:leading-9 truncate sm:overflow-visible">
|
||||||
|
<span className="bg-clip-text text-transparent bg-gradient-to-br from-indigo-400 to-purple-400">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
@@ -31,7 +31,7 @@ const ListView: React.FC<ListViewProps> = ({
|
|||||||
No Results
|
No Results
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7">
|
||||||
{items?.map((title) => {
|
{items?.map((title) => {
|
||||||
let titleCard: React.ReactNode;
|
let titleCard: React.ReactNode;
|
||||||
|
|
||||||
@@ -47,6 +47,7 @@ const ListView: React.FC<ListViewProps> = ({
|
|||||||
userScore={title.voteAverage}
|
userScore={title.voteAverage}
|
||||||
year={title.releaseDate}
|
year={title.releaseDate}
|
||||||
mediaType={title.mediaType}
|
mediaType={title.mediaType}
|
||||||
|
canExpand
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -61,12 +62,17 @@ const ListView: React.FC<ListViewProps> = ({
|
|||||||
userScore={title.voteAverage}
|
userScore={title.voteAverage}
|
||||||
year={title.firstAirDate}
|
year={title.firstAirDate}
|
||||||
mediaType={title.mediaType}
|
mediaType={title.mediaType}
|
||||||
|
canExpand
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'person':
|
case 'person':
|
||||||
titleCard = (
|
titleCard = (
|
||||||
<PersonCard name={title.name} profilePath={title.profilePath} />
|
<PersonCard
|
||||||
|
name={title.name}
|
||||||
|
profilePath={title.profilePath}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,12 +88,12 @@ const ListView: React.FC<ListViewProps> = ({
|
|||||||
})}
|
})}
|
||||||
{isLoading &&
|
{isLoading &&
|
||||||
!isReachingEnd &&
|
!isReachingEnd &&
|
||||||
[...Array(10)].map((_item, i) => (
|
[...Array(20)].map((_item, i) => (
|
||||||
<li
|
<li
|
||||||
key={`placeholder-${i}`}
|
key={`placeholder-${i}`}
|
||||||
className="col-span-1 flex flex-col text-center items-center"
|
className="col-span-1 flex flex-col text-center items-center"
|
||||||
>
|
>
|
||||||
<TitleCard.Placeholder />
|
<TitleCard.Placeholder canExpand />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -4,9 +4,10 @@ import type { MovieResult } from '../../../server/models/Search';
|
|||||||
import ListView from '../Common/ListView';
|
import ListView from '../Common/ListView';
|
||||||
import { LanguageContext } from '../../context/LanguageContext';
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
discovermovies: 'Discover Movies',
|
discovermovies: 'Popular Movies',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
@@ -55,13 +56,9 @@ const DiscoverMovies: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="md:flex md:items-center md:justify-between mb-8 mt-6">
|
<Header>
|
||||||
<div className="flex-1 min-w-0">
|
<FormattedMessage {...messages.discovermovies} />
|
||||||
<h2 className="text-xl leading-7 text-white sm:text-2xl sm:leading-9 sm:truncate">
|
</Header>
|
||||||
<FormattedMessage {...messages.discovermovies} />
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ListView
|
<ListView
|
||||||
items={titles}
|
items={titles}
|
||||||
isEmpty={isEmpty}
|
isEmpty={isEmpty}
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useSWRInfinite } from 'swr';
|
import { useSWRInfinite } from 'swr';
|
||||||
import { TvResult } from '../../../server/models/Search';
|
import type { TvResult } from '../../../server/models/Search';
|
||||||
import ListView from '../Common/ListView';
|
import ListView from '../Common/ListView';
|
||||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import { LanguageContext } from '../../context/LanguageContext';
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
discovertv: 'Discover Series',
|
discovertv: 'Popular Series',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
@@ -52,13 +53,9 @@ const DiscoverTv: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="md:flex md:items-center md:justify-between mb-8 mt-6">
|
<Header>
|
||||||
<div className="flex-1 min-w-0">
|
<FormattedMessage {...messages.discovertv} />
|
||||||
<h2 className="text-xl leading-7 text-white sm:text-2xl sm:leading-9 sm:truncate">
|
</Header>
|
||||||
<FormattedMessage {...messages.discovertv} />
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ListView
|
<ListView
|
||||||
items={titles}
|
items={titles}
|
||||||
isEmpty={isEmpty}
|
isEmpty={isEmpty}
|
||||||
|
81
src/components/Discover/Trending.tsx
Normal file
81
src/components/Discover/Trending.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useSWRInfinite } from 'swr';
|
||||||
|
import type {
|
||||||
|
MovieResult,
|
||||||
|
TvResult,
|
||||||
|
PersonResult,
|
||||||
|
} from '../../../server/models/Search';
|
||||||
|
import ListView from '../Common/ListView';
|
||||||
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
trending: 'Trending',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
page: number;
|
||||||
|
totalResults: number;
|
||||||
|
totalPages: number;
|
||||||
|
results: (MovieResult | TvResult | PersonResult)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Trending: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
|
||||||
|
(pageIndex: number, previousPageData: SearchResult | null) => {
|
||||||
|
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/v1/discover/trending?page=${
|
||||||
|
pageIndex + 1
|
||||||
|
}&language=${locale}`;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialSize: 3,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoadingInitialData = !data && !error;
|
||||||
|
const isLoadingMore =
|
||||||
|
isLoadingInitialData ||
|
||||||
|
(size > 0 && data && typeof data[size - 1] === 'undefined');
|
||||||
|
|
||||||
|
const fetchMore = () => {
|
||||||
|
setSize(size + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = data?.reduce(
|
||||||
|
(a, v) => [...a, ...v.results],
|
||||||
|
[] as (MovieResult | TvResult | PersonResult)[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
||||||
|
const isReachingEnd =
|
||||||
|
isEmpty || (data && data[data.length - 1]?.results.length < 20);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<FormattedMessage {...messages.trending} />
|
||||||
|
</Header>
|
||||||
|
<ListView
|
||||||
|
items={titles}
|
||||||
|
isEmpty={isEmpty}
|
||||||
|
isLoading={
|
||||||
|
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
|
||||||
|
}
|
||||||
|
isReachingEnd={isReachingEnd}
|
||||||
|
onScrollBottom={fetchMore}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Trending;
|
77
src/components/Discover/Upcoming.tsx
Normal file
77
src/components/Discover/Upcoming.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useSWRInfinite } from 'swr';
|
||||||
|
import type { MovieResult } from '../../../server/models/Search';
|
||||||
|
import ListView from '../Common/ListView';
|
||||||
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
upcomingmovies: 'Upcoming Movies',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
page: number;
|
||||||
|
totalResults: number;
|
||||||
|
totalPages: number;
|
||||||
|
results: MovieResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpcomingMovies: React.FC = () => {
|
||||||
|
const { locale } = useContext(LanguageContext);
|
||||||
|
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
|
||||||
|
(pageIndex: number, previousPageData: SearchResult | null) => {
|
||||||
|
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/v1/discover/movies/upcoming?page=${
|
||||||
|
pageIndex + 1
|
||||||
|
}&language=${locale}`;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialSize: 3,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoadingInitialData = !data && !error;
|
||||||
|
const isLoadingMore =
|
||||||
|
isLoadingInitialData ||
|
||||||
|
(size > 0 && data && typeof data[size - 1] === 'undefined');
|
||||||
|
|
||||||
|
const fetchMore = () => {
|
||||||
|
setSize(size + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = data?.reduce(
|
||||||
|
(a, v) => [...a, ...v.results],
|
||||||
|
[] as MovieResult[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
||||||
|
const isReachingEnd =
|
||||||
|
isEmpty || (data && data[data.length - 1]?.results.length < 20);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<FormattedMessage {...messages.upcomingmovies} />
|
||||||
|
</Header>
|
||||||
|
<ListView
|
||||||
|
items={titles}
|
||||||
|
isEmpty={isEmpty}
|
||||||
|
isLoading={
|
||||||
|
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
|
||||||
|
}
|
||||||
|
isReachingEnd={isReachingEnd}
|
||||||
|
onScrollBottom={fetchMore}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpcomingMovies;
|
@@ -77,27 +77,11 @@ const Discover: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
|
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Link href="/recent">
|
<div className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
||||||
<a className="inline-flex text-xl leading-7 text-cool-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
|
<span>
|
||||||
<span>
|
<FormattedMessage {...messages.recentlyAdded} />
|
||||||
<FormattedMessage {...messages.recentlyAdded} />
|
</span>
|
||||||
</span>
|
</div>
|
||||||
<svg
|
|
||||||
className="w-6 h-6 ml-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
|
@@ -4,15 +4,21 @@ interface PersonCardProps {
|
|||||||
name: string;
|
name: string;
|
||||||
subName?: string;
|
subName?: string;
|
||||||
profilePath?: string;
|
profilePath?: string;
|
||||||
|
canExpand?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PersonCard: React.FC<PersonCardProps> = ({
|
const PersonCard: React.FC<PersonCardProps> = ({
|
||||||
name,
|
name,
|
||||||
subName,
|
subName,
|
||||||
profilePath,
|
profilePath,
|
||||||
|
canExpand = false,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-36 sm:w-36 md:w-44 bg-cool-gray-600 rounded-lg text-white shadow-lg hover:bg-cool-gray-500 transition ease-in-out duration-150 cursor-pointer">
|
<div
|
||||||
|
className={`relative ${
|
||||||
|
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
|
||||||
|
} bg-cool-gray-600 rounded-lg text-white shadow-lg hover:bg-cool-gray-500 transition ease-in-out duration-150 cursor-pointer`}
|
||||||
|
>
|
||||||
<div style={{ paddingBottom: '150%' }}>
|
<div style={{ paddingBottom: '150%' }}>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
{profilePath && (
|
{profilePath && (
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Placeholder: React.FC = () => {
|
interface PlaceholderProps {
|
||||||
|
canExpand?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Placeholder: React.FC<PlaceholderProps> = ({ canExpand = false }) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative animate-pulse rounded-lg bg-cool-gray-700 w-36 sm:w-36 md:w-44 ">
|
<div
|
||||||
|
className={`relative animate-pulse rounded-lg bg-cool-gray-700 ${
|
||||||
|
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
<div className="w-full" style={{ paddingBottom: '150%' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -19,7 +19,7 @@ interface TitleCardProps {
|
|||||||
userScore: number;
|
userScore: number;
|
||||||
mediaType: MediaType;
|
mediaType: MediaType;
|
||||||
status?: MediaStatus;
|
status?: MediaStatus;
|
||||||
requestId?: number;
|
canExpand?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TitleCard: React.FC<TitleCardProps> = ({
|
const TitleCard: React.FC<TitleCardProps> = ({
|
||||||
@@ -30,7 +30,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
|||||||
title,
|
title,
|
||||||
status,
|
status,
|
||||||
mediaType,
|
mediaType,
|
||||||
requestId,
|
canExpand = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [currentStatus, setCurrentStatus] = useState(status);
|
const [currentStatus, setCurrentStatus] = useState(status);
|
||||||
@@ -55,7 +55,7 @@ const TitleCard: React.FC<TitleCardProps> = ({
|
|||||||
const closeModal = useCallback(() => setShowRequestModal(false), []);
|
const closeModal = useCallback(() => setShowRequestModal(false), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-36 sm:w-36 md:w-44">
|
<div className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}>
|
||||||
<RequestModal
|
<RequestModal
|
||||||
tmdbId={id}
|
tmdbId={id}
|
||||||
show={showRequestModal}
|
show={showRequestModal}
|
||||||
|
@@ -7,6 +7,7 @@ import Button from '../Common/Button';
|
|||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
edituser: 'Edit User',
|
edituser: 'Edit User',
|
||||||
@@ -143,184 +144,183 @@ const UserEdit: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-4 space-y-6 sm:p-6 lg:pb-8">
|
<>
|
||||||
<div className="md:flex md:items-center md:justify-between mt-8 mb-6">
|
<Header extraMargin={4}>Edit User</Header>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="px-4 space-y-6 sm:p-6 lg:pb-8">
|
||||||
<h2 className="text-2xl font-bold leading-7 text-cool-gray-100 sm:text-3xl sm:leading-9 sm:truncate">
|
<div className="flex flex-col space-y-6 lg:flex-row lg:space-y-0 lg:space-x-6 text-white">
|
||||||
<FormattedMessage {...messages.edituser} />
|
<div className="flex-grow space-y-6">
|
||||||
</h2>
|
<div className="space-y-1">
|
||||||
</div>
|
<label
|
||||||
</div>
|
htmlFor="username"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400"
|
||||||
<div className="flex flex-col space-y-6 lg:flex-row lg:space-y-0 lg:space-x-6 text-white">
|
|
||||||
<div className="flex-grow space-y-6">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor="username"
|
|
||||||
className="block text-sm font-medium leading-5 text-cool-gray-400"
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.username} />
|
|
||||||
</label>
|
|
||||||
<div className="rounded-md shadow-sm flex">
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
|
||||||
value={user?.username}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="block text-sm font-medium leading-5 text-cool-gray-400"
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.email} />
|
|
||||||
</label>
|
|
||||||
<div className="rounded-md shadow-sm flex">
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
|
||||||
value={user?.email}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
|
|
||||||
<p
|
|
||||||
className="block text-sm leading-5 font-medium text-cool-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.avatar} />
|
|
||||||
</p>
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
>
|
||||||
<img
|
<FormattedMessage {...messages.username} />
|
||||||
className="rounded-full h-full w-full"
|
</label>
|
||||||
src={user?.avatar}
|
<div className="rounded-md shadow-sm flex">
|
||||||
alt=""
|
<input
|
||||||
|
id="username"
|
||||||
|
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
value={user?.username}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...messages.email} />
|
||||||
|
</label>
|
||||||
|
<div className="rounded-md shadow-sm flex">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
className="form-input flex-grow block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
value={user?.email}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden relative rounded-full overflow-hidden lg:block transition duration-150 ease-in-out">
|
<div className="flex-grow space-y-1 lg:flex-grow-0 lg:flex-shrink-0">
|
||||||
<img
|
<p
|
||||||
className="relative rounded-full w-40 h-40"
|
className="block text-sm leading-5 font-medium text-cool-gray-400"
|
||||||
src={user?.avatar}
|
aria-hidden="true"
|
||||||
alt=""
|
>
|
||||||
/>
|
<FormattedMessage {...messages.avatar} />
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div className="lg:hidden">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
<div className="text-white">
|
|
||||||
<div className="sm:border-t sm:border-gray-200 sm:pt-5">
|
|
||||||
<div role="group" aria-labelledby="label-permissions">
|
|
||||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
|
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
className="text-base leading-6 font-medium sm:text-sm sm:leading-5"
|
className="flex-shrink-0 inline-block rounded-full overflow-hidden h-12 w-12"
|
||||||
id="label-permissions"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<FormattedMessage {...messages.permissions} />
|
<img
|
||||||
|
className="rounded-full h-full w-full"
|
||||||
|
src={user?.avatar}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
</div>
|
||||||
<div className="max-w-lg">
|
|
||||||
{permissionList.map((permissionOption) => (
|
<div className="hidden relative rounded-full overflow-hidden lg:block transition duration-150 ease-in-out">
|
||||||
<div
|
<img
|
||||||
className={`relative flex items-start first:mt-0 mt-4 ${
|
className="relative rounded-full w-40 h-40"
|
||||||
(permissionOption.permission !== Permission.ADMIN &&
|
src={user?.avatar}
|
||||||
hasPermission(Permission.ADMIN, currentPermission)) ||
|
alt=""
|
||||||
(currentUser?.id !== 1 &&
|
/>
|
||||||
permissionOption.permission === Permission.ADMIN) ||
|
</div>
|
||||||
(!currentHasPermission(Permission.MANAGE_SETTINGS) &&
|
</div>
|
||||||
permissionOption.permission ===
|
</div>
|
||||||
Permission.MANAGE_SETTINGS)
|
<div className="text-white">
|
||||||
? 'opacity-50'
|
<div className="sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
: ''
|
<div role="group" aria-labelledby="label-permissions">
|
||||||
}`}
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
|
||||||
key={`permission-option-${permissionOption.id}`}
|
<div>
|
||||||
>
|
<div
|
||||||
<div className="flex items-center h-5">
|
className="text-base leading-6 font-medium sm:text-sm sm:leading-5"
|
||||||
<input
|
id="label-permissions"
|
||||||
id={permissionOption.id}
|
>
|
||||||
name="permissions"
|
<FormattedMessage {...messages.permissions} />
|
||||||
type="checkbox"
|
</div>
|
||||||
className="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out"
|
</div>
|
||||||
disabled={
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
(permissionOption.permission !== Permission.ADMIN &&
|
<div className="max-w-lg">
|
||||||
hasPermission(
|
{permissionList.map((permissionOption) => (
|
||||||
Permission.ADMIN,
|
<div
|
||||||
currentPermission
|
className={`relative flex items-start first:mt-0 mt-4 ${
|
||||||
)) ||
|
(permissionOption.permission !== Permission.ADMIN &&
|
||||||
(currentUser?.id !== 1 &&
|
hasPermission(
|
||||||
permissionOption.permission ===
|
Permission.ADMIN,
|
||||||
Permission.ADMIN) ||
|
currentPermission
|
||||||
(!currentHasPermission(
|
)) ||
|
||||||
Permission.MANAGE_SETTINGS
|
(currentUser?.id !== 1 &&
|
||||||
) &&
|
permissionOption.permission === Permission.ADMIN) ||
|
||||||
permissionOption.permission ===
|
(!currentHasPermission(Permission.MANAGE_SETTINGS) &&
|
||||||
Permission.MANAGE_SETTINGS)
|
permissionOption.permission ===
|
||||||
}
|
Permission.MANAGE_SETTINGS)
|
||||||
onClick={() => {
|
? 'opacity-50'
|
||||||
setCurrentPermission((current) =>
|
: ''
|
||||||
hasPermission(
|
}`}
|
||||||
permissionOption.permission,
|
key={`permission-option-${permissionOption.id}`}
|
||||||
currentPermission
|
>
|
||||||
)
|
<div className="flex items-center h-5">
|
||||||
? current - permissionOption.permission
|
<input
|
||||||
: current + permissionOption.permission
|
id={permissionOption.id}
|
||||||
);
|
name="permissions"
|
||||||
}}
|
type="checkbox"
|
||||||
checked={hasPermission(
|
className="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
permissionOption.permission,
|
disabled={
|
||||||
currentPermission
|
(permissionOption.permission !==
|
||||||
)}
|
Permission.ADMIN &&
|
||||||
/>
|
hasPermission(
|
||||||
|
Permission.ADMIN,
|
||||||
|
currentPermission
|
||||||
|
)) ||
|
||||||
|
(currentUser?.id !== 1 &&
|
||||||
|
permissionOption.permission ===
|
||||||
|
Permission.ADMIN) ||
|
||||||
|
(!currentHasPermission(
|
||||||
|
Permission.MANAGE_SETTINGS
|
||||||
|
) &&
|
||||||
|
permissionOption.permission ===
|
||||||
|
Permission.MANAGE_SETTINGS)
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentPermission((current) =>
|
||||||
|
hasPermission(
|
||||||
|
permissionOption.permission,
|
||||||
|
currentPermission
|
||||||
|
)
|
||||||
|
? current - permissionOption.permission
|
||||||
|
: current + permissionOption.permission
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
checked={hasPermission(
|
||||||
|
permissionOption.permission,
|
||||||
|
currentPermission
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm leading-5">
|
||||||
|
<label
|
||||||
|
htmlFor={permissionOption.id}
|
||||||
|
className="font-medium"
|
||||||
|
>
|
||||||
|
{permissionOption.name}
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
{permissionOption.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 text-sm leading-5">
|
))}
|
||||||
<label
|
</div>
|
||||||
htmlFor={permissionOption.id}
|
|
||||||
className="font-medium"
|
|
||||||
>
|
|
||||||
{permissionOption.name}
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-500">
|
|
||||||
{permissionOption.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-8 border-t border-cool-gray-700 pt-5">
|
||||||
<div className="mt-8 border-t border-cool-gray-700 pt-5">
|
<div className="flex justify-end">
|
||||||
<div className="flex justify-end">
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
<Button
|
||||||
<Button
|
buttonType="primary"
|
||||||
buttonType="primary"
|
type="submit"
|
||||||
type="submit"
|
disabled={isUpdating}
|
||||||
disabled={isUpdating}
|
onClick={() => updateUser()}
|
||||||
onClick={() => updateUser()}
|
>
|
||||||
>
|
{isUpdating
|
||||||
{isUpdating
|
? intl.formatMessage(messages.saving)
|
||||||
? intl.formatMessage(messages.saving)
|
: intl.formatMessage(messages.save)}
|
||||||
: intl.formatMessage(messages.save)}
|
</Button>
|
||||||
</Button>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import Button from '../Common/Button';
|
|||||||
import { hasPermission } from '../../../server/lib/permissions';
|
import { hasPermission } from '../../../server/lib/permissions';
|
||||||
import { Permission } from '../../hooks/useUser';
|
import { Permission } from '../../hooks/useUser';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
|
||||||
const UserList: React.FC = () => {
|
const UserList: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -19,13 +20,7 @@ const UserList: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="md:flex md:items-center md:justify-between mt-8 mb-6">
|
<Header extraMargin={4}>User List</Header>
|
||||||
<div className="flex-1 min-w-0 mx-4">
|
|
||||||
<h2 className="text-2xl font-bold leading-7 text-cool-gray-100 sm:text-3xl sm:leading-9 sm:truncate">
|
|
||||||
User List
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
|
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"components.Discover.discovermovies": "Discover Movies",
|
"components.Discover.discovermovies": "Popular Movies",
|
||||||
"components.Discover.discovertv": "Discover Series",
|
"components.Discover.discovertv": "Popular Series",
|
||||||
"components.Discover.nopending": "No Pending Requests",
|
"components.Discover.nopending": "No Pending Requests",
|
||||||
"components.Discover.popularmovies": "Popular Movies",
|
"components.Discover.popularmovies": "Popular Movies",
|
||||||
"components.Discover.populartv": "Popular Series",
|
"components.Discover.populartv": "Popular Series",
|
||||||
"components.Discover.recentlyAdded": "Recently Added",
|
"components.Discover.recentlyAdded": "Recently Added",
|
||||||
"components.Discover.recentrequests": "Recent Requests",
|
"components.Discover.recentrequests": "Recent Requests",
|
||||||
|
"components.Discover.trending": "Trending",
|
||||||
"components.Discover.upcoming": "Upcoming Movies",
|
"components.Discover.upcoming": "Upcoming Movies",
|
||||||
"components.Layout.LanguagePicker.changelanguage": "Change Language",
|
"components.Layout.LanguagePicker.changelanguage": "Change Language",
|
||||||
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
||||||
"components.Layout.Sidebar.dashboard": "Dashboard",
|
"components.Layout.Sidebar.dashboard": "Discover",
|
||||||
"components.Layout.Sidebar.requests": "Requests",
|
"components.Layout.Sidebar.requests": "Requests",
|
||||||
"components.Layout.Sidebar.settings": "Settings",
|
"components.Layout.Sidebar.settings": "Settings",
|
||||||
"components.Layout.Sidebar.users": "Users",
|
"components.Layout.Sidebar.users": "Users",
|
||||||
@@ -76,6 +77,29 @@
|
|||||||
"components.TvDetails.status": "Status",
|
"components.TvDetails.status": "Status",
|
||||||
"components.TvDetails.unavailable": "Unavailable",
|
"components.TvDetails.unavailable": "Unavailable",
|
||||||
"components.TvDetails.userrating": "User Rating",
|
"components.TvDetails.userrating": "User Rating",
|
||||||
|
"components.UserEdit.admin": "Admin",
|
||||||
|
"components.UserEdit.adminDescription": "Full administrator access. Bypasses all permission checks.",
|
||||||
|
"components.UserEdit.autoapprove": "Auto Approve",
|
||||||
|
"components.UserEdit.autoapproveDescription": "Grants auto approval for any requests made by this user.",
|
||||||
|
"components.UserEdit.avatar": "Avatar",
|
||||||
|
"components.UserEdit.edituser": "Edit User",
|
||||||
|
"components.UserEdit.email": "Email",
|
||||||
|
"components.UserEdit.managerequests": "Manage Requests",
|
||||||
|
"components.UserEdit.managerequestsDescription": "Grants permission to manage Overseerr requests. This includes approving and denying requests.",
|
||||||
|
"components.UserEdit.permissions": "Permissions",
|
||||||
|
"components.UserEdit.request": "Request",
|
||||||
|
"components.UserEdit.requestDescription": "Grants permission to make requests for movies or tv shows.",
|
||||||
|
"components.UserEdit.save": "Save",
|
||||||
|
"components.UserEdit.saving": "Saving...",
|
||||||
|
"components.UserEdit.settings": "Manage Settings",
|
||||||
|
"components.UserEdit.settingsDescription": "Grants permission to modify all Overseerr settings. User must have this permission to be able to grant it to others.",
|
||||||
|
"components.UserEdit.userfail": "Something went wrong saving the user.",
|
||||||
|
"components.UserEdit.username": "Username",
|
||||||
|
"components.UserEdit.users": "Manage Users",
|
||||||
|
"components.UserEdit.usersDescription": "Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.",
|
||||||
|
"components.UserEdit.usersaved": "User succesfully saved",
|
||||||
|
"components.UserEdit.vote": "Vote",
|
||||||
|
"components.UserEdit.voteDescription": "Grants permission to vote on requests (voting not yet implemented)",
|
||||||
"pages.internalServerError": "{statusCode} - Internal Server Error",
|
"pages.internalServerError": "{statusCode} - Internal Server Error",
|
||||||
"pages.oops": "Oops",
|
"pages.oops": "Oops",
|
||||||
"pages.pageNotFound": "404 - Page Not Found",
|
"pages.pageNotFound": "404 - Page Not Found",
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
"components.Discover.populartv": "人気のテレビ番組",
|
"components.Discover.populartv": "人気のテレビ番組",
|
||||||
"components.Discover.recentlyAdded": "",
|
"components.Discover.recentlyAdded": "",
|
||||||
"components.Discover.recentrequests": "最近のリクエスト",
|
"components.Discover.recentrequests": "最近のリクエスト",
|
||||||
|
"components.Discover.trending": "",
|
||||||
"components.Discover.upcoming": "",
|
"components.Discover.upcoming": "",
|
||||||
"components.Layout.LanguagePicker.changelanguage": "言語",
|
"components.Layout.LanguagePicker.changelanguage": "言語",
|
||||||
"components.Layout.SearchInput.searchPlaceholder": "作品名で検索",
|
"components.Layout.SearchInput.searchPlaceholder": "作品名で検索",
|
||||||
@@ -76,6 +77,29 @@
|
|||||||
"components.TvDetails.status": "",
|
"components.TvDetails.status": "",
|
||||||
"components.TvDetails.unavailable": "",
|
"components.TvDetails.unavailable": "",
|
||||||
"components.TvDetails.userrating": "",
|
"components.TvDetails.userrating": "",
|
||||||
|
"components.UserEdit.admin": "",
|
||||||
|
"components.UserEdit.adminDescription": "",
|
||||||
|
"components.UserEdit.autoapprove": "",
|
||||||
|
"components.UserEdit.autoapproveDescription": "",
|
||||||
|
"components.UserEdit.avatar": "",
|
||||||
|
"components.UserEdit.edituser": "",
|
||||||
|
"components.UserEdit.email": "",
|
||||||
|
"components.UserEdit.managerequests": "",
|
||||||
|
"components.UserEdit.managerequestsDescription": "",
|
||||||
|
"components.UserEdit.permissions": "",
|
||||||
|
"components.UserEdit.request": "",
|
||||||
|
"components.UserEdit.requestDescription": "",
|
||||||
|
"components.UserEdit.save": "",
|
||||||
|
"components.UserEdit.saving": "",
|
||||||
|
"components.UserEdit.settings": "",
|
||||||
|
"components.UserEdit.settingsDescription": "",
|
||||||
|
"components.UserEdit.userfail": "",
|
||||||
|
"components.UserEdit.username": "",
|
||||||
|
"components.UserEdit.users": "",
|
||||||
|
"components.UserEdit.usersDescription": "",
|
||||||
|
"components.UserEdit.usersaved": "",
|
||||||
|
"components.UserEdit.vote": "",
|
||||||
|
"components.UserEdit.voteDescription": "",
|
||||||
"pages.internalServerError": "",
|
"pages.internalServerError": "",
|
||||||
"pages.oops": "ああ",
|
"pages.oops": "ああ",
|
||||||
"pages.pageNotFound": "",
|
"pages.pageNotFound": "",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import DiscoverMovies from '../../components/Discover/DiscoverMovies';
|
import DiscoverMovies from '../../../components/Discover/DiscoverMovies';
|
||||||
|
|
||||||
const DiscoverMoviesPage: NextPage = () => {
|
const DiscoverMoviesPage: NextPage = () => {
|
||||||
return <DiscoverMovies />;
|
return <DiscoverMovies />;
|
9
src/pages/discover/movies/upcoming.tsx
Normal file
9
src/pages/discover/movies/upcoming.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import UpcomingMovies from '../../../components/Discover/Upcoming';
|
||||||
|
|
||||||
|
const UpcomingMoviesPage: NextPage = () => {
|
||||||
|
return <UpcomingMovies />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpcomingMoviesPage;
|
9
src/pages/discover/trending.tsx
Normal file
9
src/pages/discover/trending.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { NextPage } from 'next';
|
||||||
|
import Trending from '../../components/Discover/Trending';
|
||||||
|
|
||||||
|
const TrendingPage: NextPage = () => {
|
||||||
|
return <Trending />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrendingPage;
|
Reference in New Issue
Block a user