pull/989/head
parent
8701fb20d0
commit
b557c06b0a
@ -0,0 +1,32 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UpdateUserSettingsRegions1613955393450
|
||||||
|
implements MigrationInterface {
|
||||||
|
name = 'UpdateUserSettingsRegions1613955393450';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_settings"("id", "enableNotifications", "discordId", "userId") SELECT "id", "enableNotifications", "discordId", "userId" FROM "user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_settings"("id", "enableNotifications", "discordId", "userId") SELECT "id", "enableNotifications", "discordId", "userId" FROM "temporary_user_settings"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useSWRInfinite } from 'swr';
|
||||||
|
import type { TvResult } from '../../../server/models/Search';
|
||||||
|
import ListView from '../Common/ListView';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { LanguageContext } from '../../context/LanguageContext';
|
||||||
|
import Header from '../Common/Header';
|
||||||
|
import useSettings from '../../hooks/useSettings';
|
||||||
|
import { MediaStatus } from '../../../server/constants/media';
|
||||||
|
import PageTitle from '../Common/PageTitle';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
upcomingtv: 'Upcoming Series',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
page: number;
|
||||||
|
totalResults: number;
|
||||||
|
totalPages: number;
|
||||||
|
results: TvResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DiscoverTvUpcoming: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const settings = useSettings();
|
||||||
|
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/tv/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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let titles = (data ?? []).reduce(
|
||||||
|
(a, v) => [...a, ...v.results],
|
||||||
|
[] as TvResult[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (settings.currentSettings.hideAvailable) {
|
||||||
|
titles = titles.filter(
|
||||||
|
(i) =>
|
||||||
|
i.mediaInfo?.status !== MediaStatus.AVAILABLE &&
|
||||||
|
i.mediaInfo?.status !== MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmpty = !isLoadingInitialData && titles?.length === 0;
|
||||||
|
const isReachingEnd =
|
||||||
|
isEmpty || (data && data[data.length - 1]?.results.length < 20);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle title={intl.formatMessage(messages.upcomingtv)} />
|
||||||
|
<div className="mt-1 mb-5">
|
||||||
|
<Header>
|
||||||
|
<FormattedMessage {...messages.upcomingtv} />
|
||||||
|
</Header>
|
||||||
|
</div>
|
||||||
|
<ListView
|
||||||
|
items={titles}
|
||||||
|
isEmpty={isEmpty}
|
||||||
|
isReachingEnd={isReachingEnd}
|
||||||
|
isLoading={
|
||||||
|
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
|
||||||
|
}
|
||||||
|
onScrollBottom={fetchMore}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscoverTvUpcoming;
|
@ -0,0 +1,185 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
|
import { countryCodeEmoji } from 'country-code-emoji';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import type { Region } from '../../../server/lib/settings';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
regionDefault: 'All',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RegionSelectorProps {
|
||||||
|
value: string;
|
||||||
|
name: string;
|
||||||
|
onChange?: (fieldName: string, region: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegionSelector: React.FC<RegionSelectorProps> = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data: regions } = useSWR<Region[]>('/api/v1/regions');
|
||||||
|
const [selectedRegion, setSelectedRegion] = useState<Region | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (regions && value) {
|
||||||
|
const matchedRegion = regions.find(
|
||||||
|
(region) => region.iso_3166_1 === value
|
||||||
|
);
|
||||||
|
setSelectedRegion(matchedRegion ?? null);
|
||||||
|
}
|
||||||
|
}, [value, regions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange && regions) {
|
||||||
|
onChange(name, selectedRegion?.iso_3166_1 ?? '');
|
||||||
|
}
|
||||||
|
}, [onChange, selectedRegion, name, regions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-40 flex max-w-lg">
|
||||||
|
<div className="w-full">
|
||||||
|
<Listbox as="div" value={selectedRegion} onChange={setSelectedRegion}>
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<span className="inline-block w-full rounded-md shadow-sm">
|
||||||
|
<Listbox.Button className="relative flex items-center w-full py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5">
|
||||||
|
{selectedRegion && (
|
||||||
|
<span className="h-4 mr-2 overflow-hidden text-lg leading-4">
|
||||||
|
{countryCodeEmoji(selectedRegion.iso_3166_1)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="block truncate">
|
||||||
|
{selectedRegion
|
||||||
|
? intl.formatDisplayName(selectedRegion.iso_3166_1, {
|
||||||
|
type: 'region',
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.regionDefault)}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className="w-5 h-5 text-gray-500"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#6b7280"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
d="M6 8l4 4 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
className="absolute w-full mt-1 bg-gray-800 rounded-md shadow-lg"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5"
|
||||||
|
>
|
||||||
|
<Listbox.Option value={null}>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
active ? 'text-white bg-indigo-600' : 'text-gray-300'
|
||||||
|
} cursor-default select-none relative py-2 pl-8 pr-4`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? 'font-semibold' : 'font-normal'
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.regionDefault)}
|
||||||
|
</span>
|
||||||
|
{selected && (
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
active ? 'text-white' : 'text-indigo-600'
|
||||||
|
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
{regions?.map((region) => (
|
||||||
|
<Listbox.Option key={region.iso_3166_1} value={region}>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
active
|
||||||
|
? 'text-white bg-indigo-600'
|
||||||
|
: 'text-gray-300'
|
||||||
|
} cursor-default select-none relative py-2 pl-8 pr-4 flex items-center`}
|
||||||
|
>
|
||||||
|
<span className="mr-2 text-lg">
|
||||||
|
{countryCodeEmoji(region.iso_3166_1)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? 'font-semibold' : 'font-normal'
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{intl.formatDisplayName(region.iso_3166_1, {
|
||||||
|
type: 'region',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
{selected && (
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
active ? 'text-white' : 'text-indigo-600'
|
||||||
|
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegionSelector;
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { NextPage } from 'next';
|
|
||||||
import DiscoverTv from '../../components/Discover/DiscoverTv';
|
|
||||||
|
|
||||||
const DiscoverMoviesPage: NextPage = () => {
|
|
||||||
return <DiscoverTv />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DiscoverMoviesPage;
|
|
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import DiscoverTv from '../../../components/Discover/DiscoverTv';
|
||||||
|
|
||||||
|
const DiscoverTvPage: NextPage = () => {
|
||||||
|
return <DiscoverTv />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscoverTvPage;
|
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import DiscoverTvUpcoming from '../../../components/Discover/DiscoverTvUpcoming';
|
||||||
|
|
||||||
|
const DiscoverTvPage: NextPage = () => {
|
||||||
|
return <DiscoverTvUpcoming />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscoverTvPage;
|
Loading…
Reference in new issue