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