diff --git a/cypress/e2e/settings/discover-customization.cy.ts b/cypress/e2e/settings/discover-customization.cy.ts new file mode 100644 index 000000000..8c96b6e3c --- /dev/null +++ b/cypress/e2e/settings/discover-customization.cy.ts @@ -0,0 +1,151 @@ +describe('Discover Customization', () => { + beforeEach(() => { + cy.loginAsAdmin(); + cy.intercept('/api/v1/settings/discover').as('getDiscoverSliders'); + }); + + it('show the discover customization settings', () => { + cy.visit('/settings'); + + cy.get('[data-testid=discover-customization]') + .should('contain', 'Discover Customization') + .scrollIntoView(); + + // There should be some built in options + cy.get('[data-testid=discover-option]').should('contain', 'Recently Added'); + cy.get('[data-testid=discover-option]').should( + 'contain', + 'Recent Requests' + ); + }); + + it('can drag to re-order elements and save to persist the changes', () => { + let dataTransfer = new DataTransfer(); + cy.visit('/settings'); + + cy.get('[data-testid=discover-option]') + .first() + .trigger('dragstart', { dataTransfer }); + cy.get('[data-testid=discover-option]') + .eq(1) + .trigger('drop', { dataTransfer }); + cy.get('[data-testid=discover-option]') + .eq(1) + .trigger('dragend', { dataTransfer }); + + cy.get('[data-testid=discover-option]') + .eq(1) + .should('contain', 'Recently Added'); + + cy.get('[data-testid=discover-customize-submit').click(); + cy.wait('@getDiscoverSliders'); + + cy.reload(); + + dataTransfer = new DataTransfer(); + + cy.get('[data-testid=discover-option]') + .eq(1) + .should('contain', 'Recently Added'); + + cy.get('[data-testid=discover-option]') + .first() + .trigger('dragstart', { dataTransfer }); + cy.get('[data-testid=discover-option]') + .eq(1) + .trigger('drop', { dataTransfer }); + cy.get('[data-testid=discover-option]') + .eq(1) + .trigger('dragend', { dataTransfer }); + + cy.get('[data-testid=discover-option]') + .eq(1) + .should('contain', 'Recent Requests'); + + cy.get('[data-testid=discover-customize-submit').click(); + cy.wait('@getDiscoverSliders'); + }); + + it('can create a new discover option and remove it', () => { + cy.visit('/settings'); + cy.intercept('/api/v1/settings/discover/*').as('discoverSlider'); + cy.intercept('/api/v1/search/keyword*').as('searchKeyword'); + + const sliderTitle = 'Custom Keyword Slider'; + + cy.get('#sliderType').select('TMDB Movie Keyword'); + + cy.get('#title').type(sliderTitle); + // First confirm that an invalid keyword doesn't allow us to submit anything + cy.get('#data').type('invalidkeyword{enter}', { delay: 100 }); + cy.wait('@searchKeyword'); + + cy.get('[data-testid=create-discover-option-form]') + .find('button') + .should('be.disabled'); + + cy.get('#data').clear(); + cy.get('#data').type('time travel{enter}', { delay: 100 }); + + // Confirming we have some results + cy.contains('.slider-header', sliderTitle) + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]'); + + cy.get('[data-testid=create-discover-option-form]').submit(); + + cy.wait('@discoverSlider'); + cy.wait('@getDiscoverSliders'); + cy.wait(1000); + + cy.get('[data-testid=discover-option]') + .first() + .should('contain', sliderTitle); + + // Make sure its still there even if we reload + cy.reload(); + + cy.get('[data-testid=discover-option]') + .first() + .should('contain', sliderTitle); + + // Verify it's not rendering on our discover page (its still disabled!) + cy.visit('/'); + + cy.get('.slider-header').should('not.contain', sliderTitle); + + cy.visit('/settings'); + + // Enable it, and check again + cy.get('[data-testid=discover-option]') + .first() + .find('[role="checkbox"]') + .click(); + + cy.get('[data-testid=discover-customize-submit').click(); + cy.wait('@getDiscoverSliders'); + + cy.visit('/'); + + cy.contains('.slider-header', sliderTitle) + .next('[data-testid=media-slider]') + .find('[data-testid=title-card]'); + + cy.visit('/settings'); + + // let's delete it and confirm its deleted. + cy.get('[data-testid=discover-option]') + .first() + .find('button') + .should('contain', 'Remove') + .click(); + + cy.wait('@discoverSlider'); + cy.wait('@getDiscoverSliders'); + cy.wait(1000); + + cy.get('[data-testid=discover-option]') + .first() + .should('not.contain', sliderTitle); + }); +}); diff --git a/cypress/e2e/settings/general-settings.cy.ts b/cypress/e2e/settings/general-settings.cy.ts index 3717f65b0..bcfce1a32 100644 --- a/cypress/e2e/settings/general-settings.cy.ts +++ b/cypress/e2e/settings/general-settings.cy.ts @@ -16,7 +16,7 @@ describe('General Settings', () => { cy.visit('/settings'); cy.get('#trustProxy').click(); - cy.get('form').submit(); + cy.get('[data-testid=settings-main-form]').submit(); cy.get('[data-testid=modal-title]').should( 'contain', 'Server Restart Required' @@ -26,7 +26,7 @@ describe('General Settings', () => { cy.get('[data-testid=modal-title]').should('not.exist'); cy.get('[type=checkbox]#trustProxy').click(); - cy.get('form').submit(); + cy.get('[data-testid=settings-main-form]').submit(); cy.get('[data-testid=modal-title]').should('not.exist'); }); }); diff --git a/overseerr-api.yml b/overseerr-api.yml index f114cce1a..fb4e91aff 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -600,6 +600,17 @@ components: name: type: string example: Adventure + Company: + type: object + properties: + id: + type: number + example: 1 + logo_path: + type: string + nullable: true + name: + type: string ProductionCompany: type: object properties: @@ -1780,6 +1791,31 @@ components: message: type: string example: A comment + DiscoverSlider: + type: object + properties: + id: + type: number + example: 1 + type: + type: number + example: 1 + title: + type: string + nullable: true + isBuiltIn: + type: boolean + enabled: + type: boolean + data: + type: string + example: '1234' + nullable: true + required: + - type + - enabled + - title + - data securitySchemes: cookieAuth: type: apiKey @@ -3042,6 +3078,104 @@ paths: responses: '204': description: Test notification attempted + /settings/discover: + get: + summary: Get all discover sliders + description: Returns all discovery sliders. Built-in and custom made. + tags: + - settings + responses: + '200': + description: Returned all discovery sliders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + post: + summary: Batch update all sliders. + description: | + Batch update all sliders at once. Should also be used for creation. Will only update sliders provided + and will not delete any sliders not present in the request. If a slider is missing a required field, + it will be ignored. Requires the `ADMIN` permission. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + responses: + '200': + description: Returned all newly updated discovery sliders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/{sliderId}: + delete: + summary: Delete slider by ID + description: Deletes the slider with the provided sliderId. Requires the `ADMIN` permission. + tags: + - settings + parameters: + - in: path + name: sliderId + required: true + schema: + type: number + responses: + '200': + description: Slider successfully deleted + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/add: + post: + summary: Add a new slider + description: | + Add a single slider and return the newly created slider. Requires the `ADMIN` permission. + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: 'New Slider' + type: + type: number + example: 1 + data: + type: string + example: '1' + responses: + '200': + description: Returns newly added discovery slider + content: + application/json: + schema: + $ref: '#/components/schemas/DiscoverSlider' + /settings/discover/reset: + get: + summary: Reset all discover sliders + description: Resets all discovery sliders to the default values. Requires the `ADMIN` permission. + tags: + - settings + responses: + '204': + description: All sliders reset to defaults /settings/about: get: summary: Get server stats @@ -3862,6 +3996,86 @@ paths: - $ref: '#/components/schemas/MovieResult' - $ref: '#/components/schemas/TvResult' - $ref: '#/components/schemas/PersonResult' + /search/keyword: + get: + summary: Search for keywords + description: Returns a list of TMDB keywords matching the search query + tags: + - search + parameters: + - in: query + name: query + required: true + schema: + type: string + example: 'christmas' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/Keyword' + /search/company: + get: + summary: Search for companies + description: Returns a list of TMDB companies matching the search query. (Will not return origin country) + tags: + - search + parameters: + - in: query + name: query + required: true + schema: + type: string + example: 'Disney' + - in: query + name: page + schema: + type: number + example: 1 + default: 1 + responses: + '200': + description: Results + content: + application/json: + schema: + type: object + properties: + page: + type: number + example: 1 + totalPages: + type: number + example: 20 + totalResults: + type: number + example: 200 + results: + type: array + items: + $ref: '#/components/schemas/Company' /discover/movies: get: summary: Discover movies @@ -3890,6 +4104,11 @@ paths: schema: type: number example: 1 + - in: query + name: keywords + schema: + type: string + example: 1,2 responses: '200': description: Results @@ -4119,6 +4338,11 @@ paths: schema: type: number example: 1 + - in: query + name: keywords + schema: + type: string + example: 1,2 responses: '200': description: Results diff --git a/package.json b/package.json index 9d9fca3ba..bd88b9dd1 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "react": "18.2.0", "react-ace": "10.1.0", "react-animate-height": "2.1.2", + "react-aria": "^3.21.0", "react-dom": "18.2.0", "react-intersection-observer": "9.4.0", "react-intl": "6.0.5", diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index ea05b8ab9..bcfc06bb3 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -3,9 +3,12 @@ import cacheManager from '@server/lib/cache'; import { sortBy } from 'lodash'; import type { TmdbCollection, + TmdbCompanySearchResponse, TmdbExternalIdResponse, TmdbGenre, TmdbGenresResult, + TmdbKeyword, + TmdbKeywordSearchResponse, TmdbLanguage, TmdbMovieDetails, TmdbNetwork, @@ -41,6 +44,7 @@ interface DiscoverMovieOptions { originalLanguage?: string; genre?: number; studio?: number; + keywords?: string; sortBy?: | 'popularity.asc' | 'popularity.desc' @@ -67,6 +71,7 @@ interface DiscoverTvOptions { originalLanguage?: string; genre?: number; network?: number; + keywords?: string; sortBy?: | 'popularity.asc' | 'popularity.desc' @@ -440,6 +445,7 @@ class TheMovieDb extends ExternalAPI { originalLanguage, genre, studio, + keywords, }: DiscoverMovieOptions = {}): Promise => { try { const data = await this.get('/discover/movie', { @@ -454,6 +460,7 @@ class TheMovieDb extends ExternalAPI { 'primary_release_date.lte': primaryReleaseDateLte, with_genres: genre, with_companies: studio, + with_keywords: keywords, }, }); @@ -473,6 +480,7 @@ class TheMovieDb extends ExternalAPI { originalLanguage, genre, network, + keywords, }: DiscoverTvOptions = {}): Promise => { try { const data = await this.get('/discover/tv', { @@ -487,6 +495,7 @@ class TheMovieDb extends ExternalAPI { include_null_first_air_dates: includeEmptyReleaseDate, with_genres: genre, with_networks: network, + with_keywords: keywords, }, }); @@ -874,6 +883,74 @@ class TheMovieDb extends ExternalAPI { throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`); } } + + public async getKeywordDetails({ + keywordId, + }: { + keywordId: number; + }): Promise { + try { + const data = await this.get( + `/keyword/${keywordId}`, + undefined, + 604800 // 7 days + ); + + return data; + } catch (e) { + throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`); + } + } + + public async searchKeyword({ + query, + page = 1, + }: { + query: string; + page?: number; + }): Promise { + try { + const data = await this.get( + '/search/keyword', + { + params: { + query, + page, + }, + }, + 86400 // 24 hours + ); + + return data; + } catch (e) { + throw new Error(`[TMDB] Failed to search keyword: ${e.message}`); + } + } + + public async searchCompany({ + query, + page = 1, + }: { + query: string; + page?: number; + }): Promise { + try { + const data = await this.get( + '/search/company', + { + params: { + query, + page, + }, + }, + 86400 // 24 hours + ); + + return data; + } catch (e) { + throw new Error(`[TMDB] Failed to search companies: ${e.message}`); + } + } } export default TheMovieDb; diff --git a/server/api/themoviedb/interfaces.ts b/server/api/themoviedb/interfaces.ts index 6d005dc94..a35154167 100644 --- a/server/api/themoviedb/interfaces.ts +++ b/server/api/themoviedb/interfaces.ts @@ -428,3 +428,18 @@ export interface TmdbWatchProviderDetails { provider_id: number; provider_name: string; } + +export interface TmdbKeywordSearchResponse extends TmdbPaginatedResponse { + results: TmdbKeyword[]; +} + +// We have production companies, but the company search results return less data +export interface TmdbCompany { + id: number; + logo_path?: string; + name: string; +} + +export interface TmdbCompanySearchResponse extends TmdbPaginatedResponse { + results: TmdbCompany[]; +} diff --git a/server/constants/discover.ts b/server/constants/discover.ts new file mode 100644 index 000000000..a19f07422 --- /dev/null +++ b/server/constants/discover.ts @@ -0,0 +1,98 @@ +import type DiscoverSlider from '@server/entity/DiscoverSlider'; + +export enum DiscoverSliderType { + RECENTLY_ADDED = 1, + RECENT_REQUESTS, + PLEX_WATCHLIST, + TRENDING, + POPULAR_MOVIES, + MOVIE_GENRES, + UPCOMING_MOVIES, + STUDIOS, + POPULAR_TV, + TV_GENRES, + UPCOMING_TV, + NETWORKS, + TMDB_MOVIE_KEYWORD, + TMDB_MOVIE_GENRE, + TMDB_TV_KEYWORD, + TMDB_TV_GENRE, + TMDB_SEARCH, + TMDB_STUDIO, + TMDB_NETWORK, +} + +export const defaultSliders: Partial[] = [ + { + type: DiscoverSliderType.RECENTLY_ADDED, + enabled: true, + isBuiltIn: true, + order: 0, + }, + { + type: DiscoverSliderType.RECENT_REQUESTS, + enabled: true, + isBuiltIn: true, + order: 1, + }, + { + type: DiscoverSliderType.PLEX_WATCHLIST, + enabled: true, + isBuiltIn: true, + order: 2, + }, + { + type: DiscoverSliderType.TRENDING, + enabled: true, + isBuiltIn: true, + order: 3, + }, + { + type: DiscoverSliderType.POPULAR_MOVIES, + enabled: true, + isBuiltIn: true, + order: 4, + }, + { + type: DiscoverSliderType.MOVIE_GENRES, + enabled: true, + isBuiltIn: true, + order: 5, + }, + { + type: DiscoverSliderType.UPCOMING_MOVIES, + enabled: true, + isBuiltIn: true, + order: 6, + }, + { + type: DiscoverSliderType.STUDIOS, + enabled: true, + isBuiltIn: true, + order: 7, + }, + { + type: DiscoverSliderType.POPULAR_TV, + enabled: true, + isBuiltIn: true, + order: 8, + }, + { + type: DiscoverSliderType.TV_GENRES, + enabled: true, + isBuiltIn: true, + order: 9, + }, + { + type: DiscoverSliderType.UPCOMING_TV, + enabled: true, + isBuiltIn: true, + order: 10, + }, + { + type: DiscoverSliderType.NETWORKS, + enabled: true, + isBuiltIn: true, + order: 11, + }, +]; diff --git a/server/entity/DiscoverSlider.ts b/server/entity/DiscoverSlider.ts new file mode 100644 index 000000000..261419a07 --- /dev/null +++ b/server/entity/DiscoverSlider.ts @@ -0,0 +1,69 @@ +import type { DiscoverSliderType } from '@server/constants/discover'; +import { defaultSliders } from '@server/constants/discover'; +import { getRepository } from '@server/datasource'; +import logger from '@server/logger'; +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity() +class DiscoverSlider { + public static async bootstrapSliders(): Promise { + const sliderRepository = getRepository(DiscoverSlider); + + for (const slider of defaultSliders) { + const existingSlider = await sliderRepository.findOne({ + where: { + type: slider.type, + }, + }); + + if (!existingSlider) { + logger.info('Creating built-in discovery slider', { + label: 'Discover Slider', + slider, + }); + await sliderRepository.save(new DiscoverSlider(slider)); + } + } + } + + @PrimaryGeneratedColumn() + public id: number; + + @Column({ type: 'int' }) + public type: DiscoverSliderType; + + @Column({ type: 'int' }) + public order: number; + + @Column({ default: false }) + public isBuiltIn: boolean; + + @Column({ default: true }) + public enabled: boolean; + + @Column({ nullable: true }) + // Title is not required for built in sliders because we will + // use translations for them. + public title?: string; + + @Column({ nullable: true }) + public data?: string; + + @CreateDateColumn() + public createdAt: Date; + + @UpdateDateColumn() + public updatedAt: Date; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export default DiscoverSlider; diff --git a/server/index.ts b/server/index.ts index 12df2f1fd..93703402e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,6 @@ import PlexAPI from '@server/api/plexapi'; import dataSource, { getRepository } from '@server/datasource'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; import { Session } from '@server/entity/Session'; import { User } from '@server/entity/User'; import { startJobs } from '@server/job/schedule'; @@ -95,6 +96,9 @@ app // Start Jobs startJobs(); + // Bootstrap Discovery Sliders + await DiscoverSlider.bootstrapSliders(); + const server = express(); if (settings.main.trustProxy) { server.enable('trust proxy'); diff --git a/server/migration/1672041273674-AddDiscoverSlider.ts b/server/migration/1672041273674-AddDiscoverSlider.ts new file mode 100644 index 000000000..81cb14324 --- /dev/null +++ b/server/migration/1672041273674-AddDiscoverSlider.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDiscoverSlider1672041273674 implements MigrationInterface { + name = 'AddDiscoverSlider1672041273674'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "discover_slider" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT (0), "enabled" boolean NOT NULL DEFAULT (1), "title" varchar, "data" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "discover_slider"`); + } +} diff --git a/server/routes/discover.ts b/server/routes/discover.ts index b39a83325..428e4f7de 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -1,5 +1,6 @@ import PlexTvAPI from '@server/api/plextv'; import TheMovieDb from '@server/api/themoviedb'; +import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; @@ -48,6 +49,7 @@ const discoverRoutes = Router(); discoverRoutes.get('/movies', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); + const keywords = req.query.keywords as string; try { const data = await tmdb.getDiscoverMovies({ @@ -55,16 +57,29 @@ discoverRoutes.get('/movies', async (req, res, next) => { language: req.locale ?? (req.query.language as string), genre: req.query.genre ? Number(req.query.genre) : undefined, studio: req.query.studio ? Number(req.query.studio) : undefined, + keywords, }); const media = await Media.getRelatedMedia( data.results.map((result) => result.id) ); + let keywordData: TmdbKeyword[] = []; + if (keywords) { + const splitKeywords = keywords.split(','); + + keywordData = await Promise.all( + splitKeywords.map(async (keywordId) => { + return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); + }) + ); + } + return res.status(200).json({ page: data.page, totalPages: data.total_pages, totalResults: data.total_results, + keywords: keywordData, results: data.results.map((result) => mapMovieResult( result, @@ -294,6 +309,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res, next) => { discoverRoutes.get('/tv', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); + const keywords = req.query.keywords as string; try { const data = await tmdb.getDiscoverTv({ @@ -301,16 +317,29 @@ discoverRoutes.get('/tv', async (req, res, next) => { language: req.locale ?? (req.query.language as string), genre: req.query.genre ? Number(req.query.genre) : undefined, network: req.query.network ? Number(req.query.network) : undefined, + keywords, }); const media = await Media.getRelatedMedia( data.results.map((result) => result.id) ); + let keywordData: TmdbKeyword[] = []; + if (keywords) { + const splitKeywords = keywords.split(','); + + keywordData = await Promise.all( + splitKeywords.map(async (keywordId) => { + return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); + }) + ); + } + return res.status(200).json({ page: data.page, totalPages: data.total_pages, totalResults: data.total_results, + keywords: keywordData, results: data.results.map((result) => mapTvResult( result, diff --git a/server/routes/index.ts b/server/routes/index.ts index 9561e171b..faac1b439 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -4,6 +4,8 @@ import type { TmdbMovieResult, TmdbTvResult, } from '@server/api/themoviedb/interfaces'; +import { getRepository } from '@server/datasource'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; import type { StatusResponse } from '@server/interfaces/api/settingsInterfaces'; import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; @@ -102,6 +104,13 @@ router.get('/settings/public', async (req, res) => { return res.status(200).json(settings.fullPublicSettings); } }); +router.get('/settings/discover', isAuthenticated(), async (_req, res) => { + const sliderRepository = getRepository(DiscoverSlider); + + const sliders = await sliderRepository.find({ order: { order: 'ASC' } }); + + return res.json(sliders); +}); router.use('/settings', isAuthenticated(Permission.ADMIN), settingsRoutes); router.use('/search', isAuthenticated(), searchRoutes); router.use('/discover', isAuthenticated(), discoverRoutes); diff --git a/server/routes/search.ts b/server/routes/search.ts index 1152bce31..b9254221a 100644 --- a/server/routes/search.ts +++ b/server/routes/search.ts @@ -56,4 +56,50 @@ searchRoutes.get('/', async (req, res, next) => { } }); +searchRoutes.get('/keyword', async (req, res, next) => { + const tmdb = new TheMovieDb(); + + try { + const results = await tmdb.searchKeyword({ + query: req.query.query as string, + page: Number(req.query.page), + }); + + return res.status(200).json(results); + } catch (e) { + logger.debug('Something went wrong retrieving keyword search results', { + label: 'API', + errorMessage: e.message, + query: req.query.query, + }); + return next({ + status: 500, + message: 'Unable to retrieve keyword search results.', + }); + } +}); + +searchRoutes.get('/company', async (req, res, next) => { + const tmdb = new TheMovieDb(); + + try { + const results = await tmdb.searchCompany({ + query: req.query.query as string, + page: Number(req.query.page), + }); + + return res.status(200).json(results); + } catch (e) { + logger.debug('Something went wrong retrieving company search results', { + label: 'API', + errorMessage: e.message, + query: req.query.query, + }); + return next({ + status: 500, + message: 'Unable to retrieve company search results.', + }); + } +}); + export default searchRoutes; diff --git a/server/routes/settings/discover.ts b/server/routes/settings/discover.ts new file mode 100644 index 000000000..7d2a227da --- /dev/null +++ b/server/routes/settings/discover.ts @@ -0,0 +1,100 @@ +import { getRepository } from '@server/datasource'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; +import logger from '@server/logger'; +import { Router } from 'express'; + +const discoverSettingRoutes = Router(); + +discoverSettingRoutes.post('/', async (req, res) => { + const sliderRepository = getRepository(DiscoverSlider); + + const sliders = req.body as DiscoverSlider[]; + + if (!Array.isArray(sliders)) { + return res.status(400).json({ message: 'Invalid request body.' }); + } + + for (let x = 0; x < sliders.length; x++) { + const slider = sliders[x]; + const existingSlider = await sliderRepository.findOne({ + where: { + id: slider.id, + }, + }); + + if (existingSlider && slider.id) { + existingSlider.enabled = slider.enabled; + existingSlider.order = x; + + // Only allow changes to the following when the slider is not built in + if (!existingSlider.isBuiltIn) { + existingSlider.title = slider.title; + existingSlider.data = slider.data; + existingSlider.type = slider.type; + } + + await sliderRepository.save(existingSlider); + } else { + const newSlider = new DiscoverSlider({ + isBuiltIn: false, + data: slider.data, + title: slider.title, + enabled: slider.enabled, + order: x, + type: slider.type, + }); + await sliderRepository.save(newSlider); + } + } + + return res.json(sliders); +}); + +discoverSettingRoutes.post('/add', async (req, res) => { + const sliderRepository = getRepository(DiscoverSlider); + + const slider = req.body as DiscoverSlider; + + const newSlider = new DiscoverSlider({ + isBuiltIn: false, + data: slider.data, + title: slider.title, + enabled: false, + order: -1, + type: slider.type, + }); + await sliderRepository.save(newSlider); + + return res.json(newSlider); +}); + +discoverSettingRoutes.get('/reset', async (_req, res) => { + const sliderRepository = getRepository(DiscoverSlider); + + await sliderRepository.clear(); + await DiscoverSlider.bootstrapSliders(); + + return res.status(204).send(); +}); + +discoverSettingRoutes.delete('/:sliderId', async (req, res, next) => { + const sliderRepository = getRepository(DiscoverSlider); + + try { + const slider = await sliderRepository.findOneOrFail({ + where: { id: Number(req.params.sliderId), isBuiltIn: false }, + }); + + await sliderRepository.remove(slider); + + return res.status(204).send(); + } catch (e) { + logger.error('Something went wrong deleting a slider.', { + label: 'API', + errorMessage: e.message, + }); + next({ status: 404, message: 'Slider not found or cannot be deleted.' }); + } +}); + +export default discoverSettingRoutes; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index 7205b1896..8023ba960 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -21,6 +21,7 @@ import type { JobId, MainSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; +import discoverSettingRoutes from '@server/routes/settings/discover'; import { appDataPath } from '@server/utils/appDataVolume'; import { getAppVersion } from '@server/utils/appVersion'; import { Router } from 'express'; @@ -40,6 +41,7 @@ const settingsRoutes = Router(); settingsRoutes.use('/notifications', notificationRoutes); settingsRoutes.use('/radarr', radarrRoutes); settingsRoutes.use('/sonarr', sonarrRoutes); +settingsRoutes.use('/discover', discoverSettingRoutes); const filteredMainSettings = ( user: User, diff --git a/src/components/Common/SlideCheckbox/index.tsx b/src/components/Common/SlideCheckbox/index.tsx new file mode 100644 index 000000000..a514d6c03 --- /dev/null +++ b/src/components/Common/SlideCheckbox/index.tsx @@ -0,0 +1,38 @@ +type SlideCheckboxProps = { + onClick: () => void; + checked?: boolean; +}; + +const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => { + return ( + { + onClick(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === 'Space') { + onClick(); + } + }} + className={`relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center pt-2 focus:outline-none`} + > + + + + ); +}; + +export default SlideCheckbox; diff --git a/src/components/Discover/DiscoverMovieKeyword/index.tsx b/src/components/Discover/DiscoverMovieKeyword/index.tsx new file mode 100644 index 000000000..9d22ba645 --- /dev/null +++ b/src/components/Discover/DiscoverMovieKeyword/index.tsx @@ -0,0 +1,68 @@ +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import { encodeURIExtraParams } from '@app/hooks/useSearchInput'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +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'; + +const messages = defineMessages({ + keywordMovies: '{keywordTitle} Movies', +}); + +const DiscoverMovieKeyword = () => { + const router = useRouter(); + const intl = useIntl(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + firstResultData, + } = useDiscover( + `/api/v1/discover/movies`, + { + keywords: encodeURIExtraParams(router.query.keywords as string), + } + ); + + if (error) { + return ; + } + + const title = isLoadingInitialData + ? intl.formatMessage(globalMessages.loading) + : intl.formatMessage(messages.keywordMovies, { + keywordTitle: firstResultData?.keywords + .map((k) => `${k.name[0].toUpperCase()}${k.name.substring(1)}`) + .join(', '), + }); + + return ( + <> + +
+
{title}
+
+ 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverMovieKeyword; diff --git a/src/components/Discover/DiscoverTvKeyword/index.tsx b/src/components/Discover/DiscoverTvKeyword/index.tsx new file mode 100644 index 000000000..ee6186f29 --- /dev/null +++ b/src/components/Discover/DiscoverTvKeyword/index.tsx @@ -0,0 +1,68 @@ +import Header from '@app/components/Common/Header'; +import ListView from '@app/components/Common/ListView'; +import PageTitle from '@app/components/Common/PageTitle'; +import useDiscover from '@app/hooks/useDiscover'; +import { encodeURIExtraParams } from '@app/hooks/useSearchInput'; +import globalMessages from '@app/i18n/globalMessages'; +import Error from '@app/pages/_error'; +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'; + +const messages = defineMessages({ + keywordSeries: '{keywordTitle} Series', +}); + +const DiscoverTvKeyword = () => { + const router = useRouter(); + const intl = useIntl(); + + const { + isLoadingInitialData, + isEmpty, + isLoadingMore, + isReachingEnd, + titles, + fetchMore, + error, + firstResultData, + } = useDiscover( + `/api/v1/discover/tv`, + { + keywords: encodeURIExtraParams(router.query.keywords as string), + } + ); + + if (error) { + return ; + } + + const title = isLoadingInitialData + ? intl.formatMessage(globalMessages.loading) + : intl.formatMessage(messages.keywordSeries, { + keywordTitle: firstResultData?.keywords + .map((k) => `${k.name[0].toUpperCase()}${k.name.substring(1)}`) + .join(', '), + }); + + return ( + <> + +
+
{title}
+
+ 0) + } + isReachingEnd={isReachingEnd} + onScrollBottom={fetchMore} + /> + + ); +}; + +export default DiscoverTvKeyword; diff --git a/src/components/Discover/PlexWatchlistSlider/index.tsx b/src/components/Discover/PlexWatchlistSlider/index.tsx new file mode 100644 index 000000000..02f4a47fe --- /dev/null +++ b/src/components/Discover/PlexWatchlistSlider/index.tsx @@ -0,0 +1,79 @@ +import Slider from '@app/components/Slider'; +import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; +import { UserType, useUser } from '@app/hooks/useUser'; +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; +import Link from 'next/link'; +import { defineMessages, useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages({ + plexwatchlist: 'Your Plex Watchlist', + emptywatchlist: + 'Media added to your Plex Watchlist will appear here.', +}); + +const PlexWatchlistSlider = () => { + const intl = useIntl(); + const { user } = useUser(); + + const { data: watchlistItems, error: watchlistError } = useSWR<{ + page: number; + totalPages: number; + totalResults: number; + results: WatchlistItem[]; + }>(user?.userType === UserType.PLEX ? '/api/v1/discover/watchlist' : null, { + revalidateOnMount: true, + }); + + if ( + user?.userType !== UserType.PLEX || + (watchlistItems && + watchlistItems.results.length === 0 && + !user?.settings?.watchlistSyncMovies && + !user?.settings?.watchlistSyncTv) || + watchlistError + ) { + return null; + } + + return ( + <> + + ( + + {msg} + + ), + })} + items={watchlistItems?.results.map((item) => ( + + ))} + /> + + ); +}; + +export default PlexWatchlistSlider; diff --git a/src/components/Discover/RecentRequestsSlider/index.tsx b/src/components/Discover/RecentRequestsSlider/index.tsx new file mode 100644 index 000000000..30f5e19f3 --- /dev/null +++ b/src/components/Discover/RecentRequestsSlider/index.tsx @@ -0,0 +1,49 @@ +import { sliderTitles } from '@app/components/Discover/constants'; +import RequestCard from '@app/components/RequestCard'; +import Slider from '@app/components/Slider'; +import { ArrowCircleRightIcon } from '@heroicons/react/outline'; +import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; +import Link from 'next/link'; +import { useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const RecentRequestsSlider = () => { + const intl = useIntl(); + const { data: requests, error: requestError } = + useSWR( + '/api/v1/request?filter=all&take=10&sort=modified&skip=0', + { + revalidateOnMount: true, + } + ); + + if (requests && requests.results.length === 0 && !requestError) { + return null; + } + + return ( + <> + + ( + + ))} + placeholder={} + /> + + ); +}; + +export default RecentRequestsSlider; diff --git a/src/components/Discover/RecentlyAddedSlider/index.tsx b/src/components/Discover/RecentlyAddedSlider/index.tsx new file mode 100644 index 000000000..078f86ba3 --- /dev/null +++ b/src/components/Discover/RecentlyAddedSlider/index.tsx @@ -0,0 +1,53 @@ +import Slider from '@app/components/Slider'; +import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; +import { Permission, useUser } from '@app/hooks/useUser'; +import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; +import { defineMessages, useIntl } from 'react-intl'; +import useSWR from 'swr'; + +const messages = defineMessages({ + recentlyAdded: 'Recently Added', +}); + +const RecentlyAddedSlider = () => { + const intl = useIntl(); + const { hasPermission } = useUser(); + const { data: media, error: mediaError } = useSWR( + '/api/v1/media?filter=allavailable&take=20&sort=mediaAdded', + { revalidateOnMount: true } + ); + + if ( + (media && !media.results.length && !mediaError) || + !hasPermission([Permission.MANAGE_REQUESTS, Permission.RECENT_VIEW], { + type: 'or', + }) + ) { + return null; + } + + return ( + <> +
+
+ {intl.formatMessage(messages.recentlyAdded)} +
+
+ ( + + ))} + /> + + ); +}; + +export default RecentlyAddedSlider; diff --git a/src/components/Discover/constants.ts b/src/components/Discover/constants.ts index b53c42c12..3cef94dbb 100644 --- a/src/components/Discover/constants.ts +++ b/src/components/Discover/constants.ts @@ -1,3 +1,5 @@ +import { defineMessages } from 'react-intl'; + type AvailableColors = | 'black' | 'red' @@ -61,3 +63,25 @@ export const genreColorMap: Record = { 10767: colorTones.lightgreen, // Talk 10768: colorTones.darkred, // War & Politics }; + +export const sliderTitles = defineMessages({ + recentrequests: 'Recent Requests', + popularmovies: 'Popular Movies', + populartv: 'Popular Series', + upcomingtv: 'Upcoming Series', + recentlyAdded: 'Recently Added', + upcoming: 'Upcoming Movies', + trending: 'Trending', + plexwatchlist: 'Your Plex Watchlist', + moviegenres: 'Movie Genres', + tvgenres: 'Series Genres', + studios: 'Studios', + networks: 'Networks', + tmdbmoviekeyword: 'TMDB Movie Keyword', + tmdbtvkeyword: 'TMDB Series Keyword', + tmdbmoviegenre: 'TMDB Movie Genre', + tmdbtvgenre: 'TMDB Series Genre', + tmdbnetwork: 'TMDB Network', + tmdbstudio: 'TMDB Studio', + tmdbsearch: 'TMDB Search', +}); diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 24dc6fea5..b2c1a07c2 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -1,189 +1,180 @@ +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; +import { sliderTitles } from '@app/components/Discover/constants'; import MovieGenreSlider from '@app/components/Discover/MovieGenreSlider'; import NetworkSlider from '@app/components/Discover/NetworkSlider'; +import PlexWatchlistSlider from '@app/components/Discover/PlexWatchlistSlider'; +import RecentlyAddedSlider from '@app/components/Discover/RecentlyAddedSlider'; +import RecentRequestsSlider from '@app/components/Discover/RecentRequestsSlider'; import StudioSlider from '@app/components/Discover/StudioSlider'; import TvGenreSlider from '@app/components/Discover/TvGenreSlider'; import MediaSlider from '@app/components/MediaSlider'; -import RequestCard from '@app/components/RequestCard'; -import Slider from '@app/components/Slider'; -import TmdbTitleCard from '@app/components/TitleCard/TmdbTitleCard'; -import { Permission, UserType, useUser } from '@app/hooks/useUser'; -import { ArrowCircleRightIcon } from '@heroicons/react/outline'; -import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; -import type { MediaResultsResponse } from '@server/interfaces/api/mediaInterfaces'; -import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; -import Link from 'next/link'; +import { encodeURIExtraParams } from '@app/hooks/useSearchInput'; +import { DiscoverSliderType } from '@server/constants/discover'; +import type DiscoverSlider from '@server/entity/DiscoverSlider'; import { defineMessages, useIntl } from 'react-intl'; import useSWR from 'swr'; const messages = defineMessages({ discover: 'Discover', - recentrequests: 'Recent Requests', - popularmovies: 'Popular Movies', - populartv: 'Popular Series', - upcomingtv: 'Upcoming Series', - recentlyAdded: 'Recently Added', - upcoming: 'Upcoming Movies', - trending: 'Trending', - plexwatchlist: 'Your Plex Watchlist', emptywatchlist: 'Media added to your Plex Watchlist will appear here.', }); const Discover = () => { const intl = useIntl(); - const { user, hasPermission } = useUser(); - - const { data: media, error: mediaError } = useSWR( - '/api/v1/media?filter=allavailable&take=20&sort=mediaAdded', - { revalidateOnMount: true } + const { data: discoverData, error: discoverError } = useSWR( + '/api/v1/settings/discover' ); - const { data: requests, error: requestError } = - useSWR( - '/api/v1/request?filter=all&take=10&sort=modified&skip=0', - { - revalidateOnMount: true, - } - ); - - const { data: watchlistItems, error: watchlistError } = useSWR<{ - page: number; - totalPages: number; - totalResults: number; - results: WatchlistItem[]; - }>(user?.userType === UserType.PLEX ? '/api/v1/discover/watchlist' : null, { - revalidateOnMount: true, - }); + if (!discoverData && !discoverError) { + return ; + } return ( <> - {(!media || !!media.results.length) && - !mediaError && - hasPermission([Permission.MANAGE_REQUESTS, Permission.RECENT_VIEW], { - type: 'or', - }) && ( - <> -
-
- {intl.formatMessage(messages.recentlyAdded)} -
-
- ( - - ))} - /> - - )} - {(!requests || !!requests.results.length) && !requestError && ( - <> - - ( - { + if (!slider.enabled) { + return null; + } + + switch (slider.type) { + case DiscoverSliderType.RECENTLY_ADDED: + return ; + case DiscoverSliderType.RECENT_REQUESTS: + return ; + case DiscoverSliderType.PLEX_WATCHLIST: + return ; + case DiscoverSliderType.TRENDING: + return ( + + ); + case DiscoverSliderType.POPULAR_MOVIES: + return ( + + ); + case DiscoverSliderType.MOVIE_GENRES: + return ; + case DiscoverSliderType.UPCOMING_MOVIES: + return ( + + ); + case DiscoverSliderType.STUDIOS: + return ; + case DiscoverSliderType.POPULAR_TV: + return ( + + ); + case DiscoverSliderType.TV_GENRES: + return ; + case DiscoverSliderType.UPCOMING_TV: + return ( + + ); + case DiscoverSliderType.NETWORKS: + return ; + case DiscoverSliderType.TMDB_MOVIE_KEYWORD: + return ( + + ); + case DiscoverSliderType.TMDB_TV_KEYWORD: + return ( + + ); + case DiscoverSliderType.TMDB_MOVIE_GENRE: + return ( + + ); + case DiscoverSliderType.TMDB_TV_GENRE: + return ( + + ); + case DiscoverSliderType.TMDB_STUDIO: + return ( + + ); + case DiscoverSliderType.TMDB_NETWORK: + return ( + + ); + case DiscoverSliderType.TMDB_SEARCH: + return ( + - ))} - placeholder={} - /> - - )} - {user?.userType === UserType.PLEX && - (!watchlistItems || - !!watchlistItems.results.length || - user.settings?.watchlistSyncMovies || - user.settings?.watchlistSyncTv) && - !watchlistError && ( - <> - - ( - - {msg} - - ), - })} - items={watchlistItems?.results.map((item) => ( - - ))} - /> - - )} - - - - - - - - - + ); + } + })} ); }; diff --git a/src/components/MediaSlider/index.tsx b/src/components/MediaSlider/index.tsx index 9a9bc054c..734579374 100644 --- a/src/components/MediaSlider/index.tsx +++ b/src/components/MediaSlider/index.tsx @@ -27,14 +27,18 @@ interface MediaSliderProps { linkUrl?: string; sliderKey: string; hideWhenEmpty?: boolean; + extraParams?: string; + onNewTitles?: (titleCount: number) => void; } const MediaSlider = ({ title, url, linkUrl, + extraParams, sliderKey, hideWhenEmpty = false, + onNewTitles, }: MediaSliderProps) => { const settings = useSettings(); const { data, error, setSize, size } = useSWRInfinite( @@ -43,7 +47,9 @@ const MediaSlider = ({ return null; } - return `${url}?page=${pageIndex + 1}`; + return `${url}?page=${pageIndex + 1}${ + extraParams ? `&${extraParams}` : '' + }`; }, { initialSize: 2, @@ -72,7 +78,13 @@ const MediaSlider = ({ ) { setSize(size + 1); } - }, [titles, setSize, size, data]); + + if (onNewTitles) { + // We aren't reporting all titles. We just want to know if there are any titles + // at all for our purposes. + onNewTitles(titles.length); + } + }, [titles, setSize, size, data, onNewTitles]); if (hideWhenEmpty && (data?.[0].results ?? []).length === 0) { return null; diff --git a/src/components/Settings/SettingsMain/DiscoverCustomization/CreateSlider/index.tsx b/src/components/Settings/SettingsMain/DiscoverCustomization/CreateSlider/index.tsx new file mode 100644 index 000000000..a2d0fdd2e --- /dev/null +++ b/src/components/Settings/SettingsMain/DiscoverCustomization/CreateSlider/index.tsx @@ -0,0 +1,383 @@ +import Button from '@app/components/Common/Button'; +import Tooltip from '@app/components/Common/Tooltip'; +import { sliderTitles } from '@app/components/Discover/constants'; +import MediaSlider from '@app/components/MediaSlider'; +import { encodeURIExtraParams } from '@app/hooks/useSearchInput'; +import type { + TmdbCompanySearchResponse, + TmdbKeywordSearchResponse, +} from '@server/api/themoviedb/interfaces'; +import { DiscoverSliderType } from '@server/constants/discover'; +import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces'; +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { debounce } from 'lodash'; +import { useCallback, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import AsyncSelect from 'react-select/async'; +import { useToasts } from 'react-toast-notifications'; +import * as Yup from 'yup'; + +const messages = defineMessages({ + addSlider: 'Add Slider', + slidernameplaceholder: 'Slider Name', + providetmdbkeywordid: 'Provide a TMDB Keyword ID', + providetmdbgenreid: 'Provide a TMDB Genre ID', + providetmdbsearch: 'Provide a search query', + providetmdbstudio: 'Provide TMDB Studio ID', + providetmdbnetwork: 'Provide TMDB Network ID', + addsuccess: 'Created new slider and saved discover customization settings.', + addfail: 'Failed to create new slider.', + needresults: 'You need to have at least 1 result to create a slider.', + validationDatarequired: 'You must provide a data value.', + validationTitlerequired: 'You must provide a title.', + addcustomslider: 'Add Custom Slider', + searchKeywords: 'Search keywords…', + seachGenres: 'Search genres…', + searchStudios: 'Search studios…', + starttyping: 'Starting typing to search.', + nooptions: 'No results.', +}); + +type CreateSliderProps = { + onCreate: () => void; +}; + +type CreateOption = { + type: DiscoverSliderType; + title: string; + dataUrl: string; + params?: string; + titlePlaceholderText: string; + dataPlaceholderText: string; +}; + +const CreateSlider = ({ onCreate }: CreateSliderProps) => { + const intl = useIntl(); + const { addToast } = useToasts(); + const [resultCount, setResultCount] = useState(0); + + const CreateSliderSchema = Yup.object().shape({ + title: Yup.string().required( + intl.formatMessage(messages.validationTitlerequired) + ), + data: Yup.string().required( + intl.formatMessage(messages.validationDatarequired) + ), + }); + + const updateResultCount = useCallback( + (count: number) => { + setResultCount(count); + }, + [setResultCount] + ); + + const loadKeywordOptions = debounce(async (inputValue: string) => { + const results = await axios.get( + '/api/v1/search/keyword', + { + params: { + query: encodeURIExtraParams(inputValue), + }, + } + ); + + return results.data.results.map((result) => ({ + label: result.name, + value: result.id, + })); + }, 100); + + const loadCompanyOptions = debounce(async (inputValue: string) => { + const results = await axios.get( + '/api/v1/search/company', + { + params: { + query: encodeURIExtraParams(inputValue), + }, + } + ); + + return results.data.results.map((result) => ({ + label: result.name, + value: result.id, + })); + }, 100); + + const loadMovieGenreOptions = async () => { + const results = await axios.get( + '/api/v1/discover/genreslider/movie' + ); + + return results.data.map((result) => ({ + label: result.name, + value: result.id, + })); + }; + + const loadTvGenreOptions = async () => { + const results = await axios.get( + '/api/v1/discover/genreslider/tv' + ); + + return results.data.map((result) => ({ + label: result.name, + value: result.id, + })); + }; + + const options: CreateOption[] = [ + { + type: DiscoverSliderType.TMDB_MOVIE_KEYWORD, + title: intl.formatMessage(sliderTitles.tmdbmoviekeyword), + dataUrl: '/api/v1/discover/movies', + params: 'keywords=$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbkeywordid), + }, + { + type: DiscoverSliderType.TMDB_TV_KEYWORD, + title: intl.formatMessage(sliderTitles.tmdbtvkeyword), + dataUrl: '/api/v1/discover/tv', + params: 'keywords=$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbkeywordid), + }, + { + type: DiscoverSliderType.TMDB_MOVIE_GENRE, + title: intl.formatMessage(sliderTitles.tmdbmoviegenre), + dataUrl: '/api/v1/discover/movies/genre/$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbgenreid), + }, + { + type: DiscoverSliderType.TMDB_TV_GENRE, + title: intl.formatMessage(sliderTitles.tmdbtvgenre), + dataUrl: '/api/v1/discover/tv/genre/$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbgenreid), + }, + { + type: DiscoverSliderType.TMDB_STUDIO, + title: intl.formatMessage(sliderTitles.tmdbstudio), + dataUrl: '/api/v1/discover/movies/studio/$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbstudio), + }, + { + type: DiscoverSliderType.TMDB_NETWORK, + title: intl.formatMessage(sliderTitles.tmdbnetwork), + dataUrl: '/api/v1/discover/tv/network/$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbnetwork), + }, + { + type: DiscoverSliderType.TMDB_SEARCH, + title: intl.formatMessage(sliderTitles.tmdbsearch), + dataUrl: '/api/v1/search', + params: 'query=$value', + titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder), + dataPlaceholderText: intl.formatMessage(messages.providetmdbsearch), + }, + ]; + + return ( + { + try { + await axios.post('/api/v1/settings/discover/add', { + type: Number(values.sliderType), + title: values.title, + data: values.data, + }); + + addToast(intl.formatMessage(messages.addsuccess), { + appearance: 'success', + autoDismiss: true, + }); + onCreate(); + resetForm(); + } catch (e) { + addToast(intl.formatMessage(messages.addfail), { + appearance: 'error', + autoDismiss: true, + }); + } + }} + > + {({ values, isValid, isSubmitting, errors, touched, setFieldValue }) => { + const activeOption = options.find( + (option) => option.type === Number(values.sliderType) + ); + + let dataInput: React.ReactNode; + + switch (activeOption?.type) { + case DiscoverSliderType.TMDB_MOVIE_KEYWORD: + case DiscoverSliderType.TMDB_TV_KEYWORD: + dataInput = ( + + inputValue === '' + ? intl.formatMessage(messages.starttyping) + : intl.formatMessage(messages.nooptions) + } + loadOptions={loadKeywordOptions} + placeholder={intl.formatMessage(messages.searchKeywords)} + onChange={(value) => { + const keywords = value.map((item) => item.value).join(','); + + setFieldValue('data', keywords); + }} + /> + ); + break; + case DiscoverSliderType.TMDB_MOVIE_GENRE: + dataInput = ( + { + setFieldValue('data', value?.value); + }} + /> + ); + break; + case DiscoverSliderType.TMDB_TV_GENRE: + dataInput = ( + { + setFieldValue('data', value?.value); + }} + /> + ); + break; + case DiscoverSliderType.TMDB_STUDIO: + dataInput = ( + { + setFieldValue('data', value?.value); + }} + /> + ); + break; + default: + dataInput = ( + + ); + } + + return ( +
+
+ + {intl.formatMessage(messages.addcustomslider)} + + + {options.map((option) => ( + + ))} + + + {errors.title && + touched.title && + typeof errors.title === 'string' && ( +
{errors.title}
+ )} + {dataInput} + {errors.data && + touched.data && + typeof errors.data === 'string' && ( +
{errors.data}
+ )} +
+ {resultCount === 0 ? ( + +
+ +
+
+ ) : ( +
+ +
+ )} +
+ +
+ {activeOption && values.title && values.data && ( + + )} +
+
+ ); + }} +
+ ); +}; + +export default CreateSlider; diff --git a/src/components/Settings/SettingsMain/DiscoverCustomization/DiscoverOption/index.tsx b/src/components/Settings/SettingsMain/DiscoverCustomization/DiscoverOption/index.tsx new file mode 100644 index 000000000..02b24084e --- /dev/null +++ b/src/components/Settings/SettingsMain/DiscoverCustomization/DiscoverOption/index.tsx @@ -0,0 +1,170 @@ +import Badge from '@app/components/Common/Badge'; +import Button from '@app/components/Common/Button'; +import SlideCheckbox from '@app/components/Common/SlideCheckbox'; +import Tooltip from '@app/components/Common/Tooltip'; +import { MenuIcon, XIcon } from '@heroicons/react/solid'; +import axios from 'axios'; +import { useRef, useState } from 'react'; +import { useDrag, useDrop } from 'react-aria'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; + +const messages = defineMessages({ + deletesuccess: 'Sucessfully deleted slider.', + deletefail: 'Failed to delete slider.', + remove: 'Remove', + enable: 'Toggle Visibility', +}); + +const Position = { + None: 'None', + Above: 'Above', + Below: 'Below', +} as const; + +type DiscoverOptionProps = { + id: number; + title: string; + subtitle?: string; + data?: string; + enabled?: boolean; + isBuiltIn?: boolean; + onEnable: () => void; + onDelete: () => void; + onPositionUpdate: ( + updatedItemId: number, + position: keyof typeof Position + ) => void; +}; + +const DiscoverOption = ({ + id, + title, + enabled, + onPositionUpdate, + onEnable, + subtitle, + data, + isBuiltIn, + onDelete, +}: DiscoverOptionProps) => { + const intl = useIntl(); + const { addToast } = useToasts(); + const ref = useRef(null); + const [hoverPosition, setHoverPosition] = useState( + Position.None + ); + + const { dragProps, isDragging } = useDrag({ + getItems() { + return [{ id: id.toString(), title }]; + }, + }); + + const deleteSlider = async () => { + try { + await axios.delete(`/api/v1/settings/discover/${id}`); + addToast(intl.formatMessage(messages.deletesuccess), { + appearance: 'success', + autoDismiss: true, + }); + onDelete(); + } catch (e) { + addToast(intl.formatMessage(messages.deletefail), { + appearance: 'error', + autoDismiss: true, + }); + } + }; + + const { dropProps } = useDrop({ + ref, + onDropMove: (e) => { + if (ref.current) { + const middlePoint = ref.current.offsetHeight / 2; + + if (e.y < middlePoint) { + setHoverPosition(Position.Above); + } else { + setHoverPosition(Position.Below); + } + } + }, + onDropExit: () => { + setHoverPosition(Position.None); + }, + onDrop: async (e) => { + const items = await Promise.all( + e.items + .filter((item) => item.kind === 'text' && item.types.has('id')) + .map(async (item) => { + if (item.kind === 'text') { + return item.getText('id'); + } + }) + ); + if (items?.[0]) { + const dropped = Number(items[0]); + onPositionUpdate(dropped, hoverPosition); + } + }, + }); + + return ( +
+ {hoverPosition === Position.Above && ( +
+ )} + {hoverPosition === Position.Below && ( +
+ )} +
+ + + {title} + {subtitle && {subtitle}} + {data && {data}} + {!isBuiltIn && ( +
+ +
+ )} + +
+ { + onEnable(); + }} + checked={enabled} + /> +
+
+
+
+ ); +}; + +export default DiscoverOption; diff --git a/src/components/Settings/SettingsMain/DiscoverCustomization/index.tsx b/src/components/Settings/SettingsMain/DiscoverCustomization/index.tsx new file mode 100644 index 000000000..be405ad8b --- /dev/null +++ b/src/components/Settings/SettingsMain/DiscoverCustomization/index.tsx @@ -0,0 +1,220 @@ +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import Tooltip from '@app/components/Common/Tooltip'; +import { sliderTitles } from '@app/components/Discover/constants'; +import CreateSlider from '@app/components/Settings/SettingsMain/DiscoverCustomization/CreateSlider'; +import DiscoverOption from '@app/components/Settings/SettingsMain/DiscoverCustomization/DiscoverOption'; +import globalMessages from '@app/i18n/globalMessages'; +import { RefreshIcon, SaveIcon } from '@heroicons/react/solid'; +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 { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; + +const messages = defineMessages({ + resettodefault: 'Reset to Default', + resetwarning: + 'Reset all sliders to default. This will also delete any custom sliders!', + updatesuccess: 'Updated discover customization settings.', + updatefailed: + 'Something went wrong updating the discover customization settings.', + resetsuccess: 'Sucessfully reset discover customization settings.', + resetfailed: + 'Something went wrong resetting the discover customization settings.', +}); + +const DiscoverCustomization = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const { data, error, mutate } = useSWR( + '/api/v1/settings/discover' + ); + const [sliders, setSliders] = useState[]>([]); + + // We need to sync the state here so that we can modify the changes locally without commiting + // anything to the server until the user decides to save the changes + useEffect(() => { + if (data) { + setSliders(data); + } + }, [data]); + + const updateSliders = async () => { + try { + await axios.post('/api/v1/settings/discover', sliders); + + addToast(intl.formatMessage(messages.updatesuccess), { + appearance: 'success', + autoDismiss: true, + }); + mutate(); + } catch (e) { + addToast(intl.formatMessage(messages.updatefailed), { + appearance: 'error', + autoDismiss: true, + }); + } + }; + + const resetSliders = async () => { + try { + await axios.get('/api/v1/settings/discover/reset'); + + addToast(intl.formatMessage(messages.resetsuccess), { + appearance: 'success', + autoDismiss: true, + }); + mutate(); + } catch (e) { + addToast(intl.formatMessage(messages.resetfailed), { + appearance: 'error', + autoDismiss: true, + }); + } + }; + + const hasChanged = () => !Object.is(data, sliders); + + const getSliderTitle = (slider: Partial): string => { + if (slider.title) { + return slider.title; + } + + switch (slider.type) { + case DiscoverSliderType.RECENTLY_ADDED: + return intl.formatMessage(sliderTitles.recentlyAdded); + case DiscoverSliderType.RECENT_REQUESTS: + return intl.formatMessage(sliderTitles.recentrequests); + case DiscoverSliderType.PLEX_WATCHLIST: + return intl.formatMessage(sliderTitles.plexwatchlist); + case DiscoverSliderType.TRENDING: + return intl.formatMessage(sliderTitles.trending); + case DiscoverSliderType.POPULAR_MOVIES: + return intl.formatMessage(sliderTitles.popularmovies); + case DiscoverSliderType.MOVIE_GENRES: + return intl.formatMessage(sliderTitles.moviegenres); + case DiscoverSliderType.UPCOMING_MOVIES: + return intl.formatMessage(sliderTitles.upcoming); + case DiscoverSliderType.STUDIOS: + return intl.formatMessage(sliderTitles.studios); + case DiscoverSliderType.POPULAR_TV: + return intl.formatMessage(sliderTitles.populartv); + case DiscoverSliderType.TV_GENRES: + return intl.formatMessage(sliderTitles.tvgenres); + case DiscoverSliderType.UPCOMING_TV: + return intl.formatMessage(sliderTitles.upcomingtv); + case DiscoverSliderType.NETWORKS: + return intl.formatMessage(sliderTitles.networks); + default: + return 'Unknown Slider'; + } + }; + + const getSliderSubtitle = ( + slider: Partial + ): string | undefined => { + switch (slider.type) { + case DiscoverSliderType.TMDB_MOVIE_KEYWORD: + return intl.formatMessage(sliderTitles.tmdbmoviekeyword); + case DiscoverSliderType.TMDB_TV_KEYWORD: + return intl.formatMessage(sliderTitles.tmdbtvkeyword); + case DiscoverSliderType.TMDB_MOVIE_GENRE: + return intl.formatMessage(sliderTitles.tmdbmoviegenre); + case DiscoverSliderType.TMDB_TV_GENRE: + return intl.formatMessage(sliderTitles.tmdbtvgenre); + case DiscoverSliderType.TMDB_STUDIO: + return intl.formatMessage(sliderTitles.tmdbstudio); + case DiscoverSliderType.TMDB_NETWORK: + return intl.formatMessage(sliderTitles.tmdbnetwork); + case DiscoverSliderType.TMDB_SEARCH: + return intl.formatMessage(sliderTitles.tmdbsearch); + default: + return undefined; + } + }; + + if (!data && !error) { + return ; + } + + return ( + <> +
+
+ {sliders.map((slider, index) => ( + { + mutate(); + }} + onEnable={() => { + const tempSliders = sliders.slice(); + tempSliders[index].enabled = !tempSliders[index].enabled; + setSliders(tempSliders); + }} + onPositionUpdate={(updatedItemId, position) => { + const originalPosition = sliders.findIndex( + (item) => item.id === updatedItemId + ); + const originalItem = sliders[originalPosition]; + + const tempSliders = sliders.slice(); + + tempSliders.splice(originalPosition, 1); + tempSliders.splice( + position === 'Above' && index > originalPosition + ? Math.max(index - 1, 0) + : index, + 0, + originalItem + ); + + setSliders(tempSliders); + }} + /> + ))} + { + mutate(); + }} + /> +
+
+
+
+ + + + + + + + +
+
+ + ); +}; + +export default DiscoverCustomization; diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain/index.tsx similarity index 96% rename from src/components/Settings/SettingsMain.tsx rename to src/components/Settings/SettingsMain/index.tsx index 7d4e188e5..352821059 100644 --- a/src/components/Settings/SettingsMain.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -7,6 +7,7 @@ import LanguageSelector from '@app/components/LanguageSelector'; import RegionSelector from '@app/components/RegionSelector'; import CopyButton from '@app/components/Settings/CopyButton'; import SettingsBadge from '@app/components/Settings/SettingsBadge'; +import DiscoverCustomization from '@app/components/Settings/SettingsMain/DiscoverCustomization'; import type { AvailableLocale } from '@app/context/LanguageContext'; import { availableLanguages } from '@app/context/LanguageContext'; import useLocale from '@app/hooks/useLocale'; @@ -55,6 +56,9 @@ const messages = defineMessages({ validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', locale: 'Display Language', + discovercustomization: 'Discover Customization', + discovercustomizationDescription: + 'Add or remove sliders on the Discover page.', }); const SettingsMain = () => { @@ -185,7 +189,7 @@ const SettingsMain = () => { setFieldValue, }) => { return ( -
+ {userHasPermission(Permission.ADMIN) && (
+
+

+ {intl.formatMessage(messages.discovercustomization)} +

+

+ {intl.formatMessage(messages.discovercustomizationDescription)} +

+
+ ); }; diff --git a/src/hooks/useSearchInput.ts b/src/hooks/useSearchInput.ts index 54876357f..a27a6d109 100644 --- a/src/hooks/useSearchInput.ts +++ b/src/hooks/useSearchInput.ts @@ -15,7 +15,7 @@ const extraEncodes: [RegExp, string][] = [ [/\*/g, '%2A'], ]; -const encodeURIExtraParams = (string: string): string => { +export const encodeURIExtraParams = (string: string): string => { let finalString = encodeURIComponent(string); extraEncodes.forEach((encode) => { diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 4ffb110e1..14a680f69 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -7,16 +7,21 @@ "components.CollectionDetails.requestcollection": "Request Collection", "components.CollectionDetails.requestcollection4k": "Request Collection in 4K", "components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Movies", + "components.Discover.DiscoverMovieKeyword.keywordMovies": "{keywordTitle} Movies", "components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Movies", "components.Discover.DiscoverNetwork.networkSeries": "{network} Series", "components.Discover.DiscoverStudio.studioMovies": "{studio} Movies", "components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series", + "components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Series", "components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series", "components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Plex Watchlist", "components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist", "components.Discover.MovieGenreList.moviegenres": "Movie Genres", "components.Discover.MovieGenreSlider.moviegenres": "Movie Genres", "components.Discover.NetworkSlider.networks": "Networks", + "components.Discover.PlexWatchlistSlider.emptywatchlist": "Media added to your Plex Watchlist will appear here.", + "components.Discover.PlexWatchlistSlider.plexwatchlist": "Your Plex Watchlist", + "components.Discover.RecentlyAddedSlider.recentlyAdded": "Recently Added", "components.Discover.StudioSlider.studios": "Studios", "components.Discover.TvGenreList.seriesgenres": "Series Genres", "components.Discover.TvGenreSlider.tvgenres": "Series Genres", @@ -24,12 +29,23 @@ "components.Discover.discovermovies": "Popular Movies", "components.Discover.discovertv": "Popular Series", "components.Discover.emptywatchlist": "Media added to your Plex Watchlist will appear here.", + "components.Discover.moviegenres": "Movie Genres", + "components.Discover.networks": "Networks", "components.Discover.plexwatchlist": "Your Plex Watchlist", "components.Discover.popularmovies": "Popular Movies", "components.Discover.populartv": "Popular Series", "components.Discover.recentlyAdded": "Recently Added", "components.Discover.recentrequests": "Recent Requests", + "components.Discover.studios": "Studios", + "components.Discover.tmdbmoviegenre": "TMDB Movie Genre", + "components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword", + "components.Discover.tmdbnetwork": "TMDB Network", + "components.Discover.tmdbsearch": "TMDB Search", + "components.Discover.tmdbstudio": "TMDB Studio", + "components.Discover.tmdbtvgenre": "TMDB Series Genre", + "components.Discover.tmdbtvkeyword": "TMDB Series Keyword", "components.Discover.trending": "Trending", + "components.Discover.tvgenres": "Series Genres", "components.Discover.upcoming": "Upcoming Movies", "components.Discover.upcomingmovies": "Upcoming Movies", "components.Discover.upcomingtv": "Upcoming Series", @@ -688,6 +704,63 @@ "components.Settings.SettingsLogs.showall": "Show All Logs", "components.Settings.SettingsLogs.time": "Timestamp", "components.Settings.SettingsLogs.viewdetails": "View Details", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.addSlider": "Add Slider", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.addcustomslider": "Add Custom Slider", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.addfail": "Failed to create new slider.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.addsuccess": "Created new slider and saved discover customization settings.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.needresults": "You need to have at least 1 result to create a slider.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.nooptions": "No results.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.providetmdbgenreid": "Provide a TMDB Genre ID", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.providetmdbkeywordid": "Provide a TMDB Keyword ID", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.providetmdbnetwork": "Provide TMDB Network ID", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.providetmdbsearch": "Provide a search query", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.providetmdbstudio": "Provide TMDB Studio ID", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.seachGenres": "Search genres…", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.searchKeywords": "Search keywords…", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.searchStudios": "Search studios…", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.slidernameplaceholder": "Slider Name", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.starttyping": "Starting typing to search.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.validationDatarequired": "You must provide a data value.", + "components.Settings.SettingsMain.DiscoverCustomization.CreateSlider.validationTitlerequired": "You must provide a title.", + "components.Settings.SettingsMain.DiscoverCustomization.DiscoverOption.deletefail": "Failed to delete slider.", + "components.Settings.SettingsMain.DiscoverCustomization.DiscoverOption.deletesuccess": "Sucessfully deleted slider.", + "components.Settings.SettingsMain.DiscoverCustomization.DiscoverOption.enable": "Toggle Visibility", + "components.Settings.SettingsMain.DiscoverCustomization.DiscoverOption.remove": "Remove", + "components.Settings.SettingsMain.DiscoverCustomization.resetfailed": "Something went wrong resetting the discover customization settings.", + "components.Settings.SettingsMain.DiscoverCustomization.resetsuccess": "Sucessfully reset discover customization settings.", + "components.Settings.SettingsMain.DiscoverCustomization.resettodefault": "Reset to Default", + "components.Settings.SettingsMain.DiscoverCustomization.resetwarning": "Reset all sliders to default. This will also delete any custom sliders!", + "components.Settings.SettingsMain.DiscoverCustomization.updatefailed": "Something went wrong updating the discover customization settings.", + "components.Settings.SettingsMain.DiscoverCustomization.updatesuccess": "Updated discover customization settings.", + "components.Settings.SettingsMain.apikey": "API Key", + "components.Settings.SettingsMain.applicationTitle": "Application Title", + "components.Settings.SettingsMain.applicationurl": "Application URL", + "components.Settings.SettingsMain.cacheImages": "Enable Image Caching", + "components.Settings.SettingsMain.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)", + "components.Settings.SettingsMain.csrfProtection": "Enable CSRF Protection", + "components.Settings.SettingsMain.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", + "components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", + "components.Settings.SettingsMain.discovercustomization": "Discover Customization", + "components.Settings.SettingsMain.discovercustomizationDescription": "Add or remove sliders on the Discover page.", + "components.Settings.SettingsMain.general": "General", + "components.Settings.SettingsMain.generalsettings": "General Settings", + "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Overseerr.", + "components.Settings.SettingsMain.hideAvailable": "Hide Available Media", + "components.Settings.SettingsMain.locale": "Display Language", + "components.Settings.SettingsMain.originallanguage": "Discover Language", + "components.Settings.SettingsMain.originallanguageTip": "Filter content by original language", + "components.Settings.SettingsMain.partialRequestsEnabled": "Allow Partial Series Requests", + "components.Settings.SettingsMain.region": "Discover Region", + "components.Settings.SettingsMain.regionTip": "Filter content by regional availability", + "components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.", + "components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!", + "components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.", + "components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!", + "components.Settings.SettingsMain.trustProxy": "Enable Proxy Support", + "components.Settings.SettingsMain.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy", + "components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title", + "components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL", + "components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.SettingsUsers.defaultPermissions": "Default Permissions", "components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users", "components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In", @@ -758,16 +831,8 @@ "components.Settings.address": "Address", "components.Settings.addsonarr": "Add Sonarr Server", "components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality", - "components.Settings.apikey": "API Key", - "components.Settings.applicationTitle": "Application Title", - "components.Settings.applicationurl": "Application URL", - "components.Settings.cacheImages": "Enable Image Caching", - "components.Settings.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)", "components.Settings.cancelscan": "Cancel Scan", "components.Settings.copied": "Copied API key to clipboard.", - "components.Settings.csrfProtection": "Enable CSRF Protection", - "components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!", - "components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", "components.Settings.currentlibrary": "Current Library: {name}", "components.Settings.default": "Default", "components.Settings.default4k": "Default 4K", @@ -777,14 +842,9 @@ "components.Settings.enablessl": "Use SSL", "components.Settings.experimentalTooltip": "Enabling this setting may result in unexpected application behavior", "components.Settings.externalUrl": "External URL", - "components.Settings.general": "General", - "components.Settings.generalsettings": "General Settings", - "components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.", - "components.Settings.hideAvailable": "Hide Available Media", "components.Settings.hostname": "Hostname or IP Address", "components.Settings.is4k": "4K", "components.Settings.librariesRemaining": "Libraries Remaining: {count}", - "components.Settings.locale": "Display Language", "components.Settings.manualscan": "Manual Library Scan", "components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!", "components.Settings.mediaTypeMovie": "movie", @@ -804,9 +864,6 @@ "components.Settings.notifications": "Notifications", "components.Settings.notificationsettings": "Notification Settings", "components.Settings.notrunning": "Not Running", - "components.Settings.originallanguage": "Discover Language", - "components.Settings.originallanguageTip": "Filter content by original language", - "components.Settings.partialRequestsEnabled": "Allow Partial Series Requests", "components.Settings.plex": "Plex", "components.Settings.plexlibraries": "Plex Libraries", "components.Settings.plexlibrariesDescription": "The libraries Overseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.", @@ -814,8 +871,6 @@ "components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Overseerr scans your Plex libraries to determine content availability.", "components.Settings.port": "Port", "components.Settings.radarrsettings": "Radarr Settings", - "components.Settings.region": "Discover Region", - "components.Settings.regionTip": "Filter content by regional availability", "components.Settings.restartrequiredTooltip": "Overseerr must be restarted for changes to this setting to take effect", "components.Settings.scan": "Sync Libraries", "components.Settings.scanning": "Syncing…", @@ -835,25 +890,16 @@ "components.Settings.tautulliApiKey": "API Key", "components.Settings.tautulliSettings": "Tautulli Settings", "components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Overseerr fetches watch history data for your Plex media from Tautulli.", - "components.Settings.toastApiKeyFailure": "Something went wrong while generating a new API key.", - "components.Settings.toastApiKeySuccess": "New API key generated successfully!", "components.Settings.toastPlexConnecting": "Attempting to connect to Plex…", "components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.", "components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!", "components.Settings.toastPlexRefresh": "Retrieving server list from Plex…", "components.Settings.toastPlexRefreshFailure": "Failed to retrieve Plex server list.", "components.Settings.toastPlexRefreshSuccess": "Plex server list retrieved successfully!", - "components.Settings.toastSettingsFailure": "Something went wrong while saving settings.", - "components.Settings.toastSettingsSuccess": "Settings saved successfully!", "components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.", "components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!", - "components.Settings.trustProxy": "Enable Proxy Support", - "components.Settings.trustProxyTip": "Allow Overseerr to correctly register client IP addresses behind a proxy", "components.Settings.urlBase": "URL Base", "components.Settings.validationApiKey": "You must provide an API key", - "components.Settings.validationApplicationTitle": "You must provide an application title", - "components.Settings.validationApplicationUrl": "You must provide a valid URL", - "components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash", "components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address", "components.Settings.validationPortRequired": "You must provide a valid port number", "components.Settings.validationUrl": "You must provide a valid URL", diff --git a/src/pages/discover/movies/keyword/index.tsx b/src/pages/discover/movies/keyword/index.tsx new file mode 100644 index 000000000..4fc0cfd27 --- /dev/null +++ b/src/pages/discover/movies/keyword/index.tsx @@ -0,0 +1,8 @@ +import DiscoverMovieKeyword from '@app/components/Discover/DiscoverMovieKeyword'; +import type { NextPage } from 'next'; + +const DiscoverMoviesKeywordPage: NextPage = () => { + return ; +}; + +export default DiscoverMoviesKeywordPage; diff --git a/src/pages/discover/tv/keyword/index.tsx b/src/pages/discover/tv/keyword/index.tsx new file mode 100644 index 000000000..0c6ab00ff --- /dev/null +++ b/src/pages/discover/tv/keyword/index.tsx @@ -0,0 +1,8 @@ +import DiscoverTvKeyword from '@app/components/Discover/DiscoverTvKeyword'; +import type { NextPage } from 'next'; + +const DiscoverTvKeywordPage: NextPage = () => { + return ; +}; + +export default DiscoverTvKeywordPage; diff --git a/src/styles/globals.css b/src/styles/globals.css index a5f417ddb..7e0900e0c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -417,6 +417,14 @@ .react-select-container .react-select__input-container { @apply text-white; } + + .react-select-container .react-select__single-value { + @apply text-sm text-gray-100; + } + + .react-select-container .react-select__placeholder { + @apply text-sm text-gray-500; + } } @layer utilities { diff --git a/yarn.lock b/yarn.lock index 3036e822a..d638cfe1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1176,6 +1176,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.6.2": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1654,6 +1661,14 @@ "@formatjs/intl-localematcher" "0.2.28" tslib "2.4.0" +"@formatjs/ecma402-abstract@1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.0.tgz#2ee584d671e2776434da88b3e1ae4ed3053ad450" + integrity sha512-o1RDlkxcLzi0ZcoaovQooZC+0M3Ox0/DKZ+YTdUU9DHgWFeEZbYXEqM9k7JHdN7VyRi4wprTVPqrK+zR/9mo8Q== + dependencies: + "@formatjs/intl-localematcher" "0.2.31" + tslib "2.4.0" + "@formatjs/ecma402-abstract@1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.4.0.tgz#ac6c17a8fffac43c6d68c849a7b732626d32654c" @@ -1675,6 +1690,22 @@ dependencies: tslib "2.4.0" +"@formatjs/fast-memoize@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.6.tgz#a442970db7e9634af556919343261a7bbe5e88c3" + integrity sha512-9CWZ3+wCkClKHX+i5j+NyoBVqGf0pIskTo6Xl6ihGokYM2yqSSS68JIgeo+99UIHc+7vi9L3/SDSz/dWI9SNlA== + dependencies: + tslib "2.4.0" + +"@formatjs/icu-messageformat-parser@2.1.11": + version "2.1.11" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.11.tgz#d0b59145bf910ea0fdd023a848369d7f08a16c26" + integrity sha512-g2OET65sDI0F3RUNXcyQPlxn+h+zQ6RkFIZZnOo70LtMEHTyDbgaMvauRlkBX52kqEe9eI99I3RaLvaM8pEcEg== + dependencies: + "@formatjs/ecma402-abstract" "1.14.0" + "@formatjs/icu-skeleton-parser" "1.3.15" + tslib "2.4.0" + "@formatjs/icu-messageformat-parser@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.4.tgz#f1e32b9937f151c1dd5c30536ce3e920b7f23813" @@ -1692,6 +1723,14 @@ "@formatjs/ecma402-abstract" "1.11.8" tslib "2.4.0" +"@formatjs/icu-skeleton-parser@1.3.15": + version "1.3.15" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.15.tgz#b30c6437aa259e8720e14beeff7e9c713d6e5646" + integrity sha512-/x7qBaswEGLEBm0vY8HmYy764py0FmD+pSzBNH5llgp1d0NFAIo+lTfsKFxPDk+iNNnL3f7ZH0KOyUtAResZ5Q== + dependencies: + "@formatjs/ecma402-abstract" "1.14.0" + tslib "2.4.0" + "@formatjs/intl-displaynames@6.0.3": version "6.0.3" resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.0.3.tgz#e648a91bccd9fb21519090eaafece3be9d15f480" @@ -1733,6 +1772,13 @@ dependencies: tslib "2.4.0" +"@formatjs/intl-localematcher@0.2.31": + version "0.2.31" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz#aada2b1e58211460cedba56889e3c489117eb6eb" + integrity sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA== + dependencies: + tslib "2.4.0" + "@formatjs/intl-numberformat@^5.5.2": version "5.7.6" resolved "https://registry.yarnpkg.com/@formatjs/intl-numberformat/-/intl-numberformat-5.7.6.tgz#630206bb0acefd2d508ccf4f82367c6875cad611" @@ -1838,6 +1884,35 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@internationalized/date@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.1.tgz#66332e9ca8f59b7be010ca65d946bca430ba4b66" + integrity sha512-E/3lASs4mAeJ2Z2ye6ab7eUD0bPUfTeNVTAv6IS+ne9UtMu9Uepb9A1U2Ae0hDr6WAlBuvUtrakaxEdYB9TV6Q== + dependencies: + "@babel/runtime" "^7.6.2" + +"@internationalized/message@^3.0.9": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.0.9.tgz#52bc20debe5296375d66ffcf56c3df5d8118a37d" + integrity sha512-yHQggKWUuSvj1GznVtie4tcYq+xMrkd/lTKCFHp6gG18KbIliDw+UI7sL9+yJPGuWiR083xuLyyhzqiPbNOEww== + dependencies: + "@babel/runtime" "^7.6.2" + intl-messageformat "^10.1.0" + +"@internationalized/number@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.1.1.tgz#160584316741de4381689ab759001603ee17b595" + integrity sha512-dBxCQKIxvsZvW2IBt3KsqrCfaw2nV6o6a8xsloJn/hjW0ayeyhKuiiMtTwW3/WGNPP7ZRyDbtuiUEjMwif1ENQ== + dependencies: + "@babel/runtime" "^7.6.2" + +"@internationalized/string@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.0.0.tgz#de563871e1b19e4d0ce3246ec18d25da1a73db73" + integrity sha512-NUSr4u+mNu5BysXFeVWZW4kvjXylPkU/YYqaWzdNuz1eABfehFiZTEYhWAAMzI3U8DTxfqF9PM3zyhk5gcfz6w== + dependencies: + "@babel/runtime" "^7.6.2" + "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" @@ -2371,6 +2446,540 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@react-aria/breadcrumbs@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.4.0.tgz#dadc6c9eb70ad7185e52929df5c82fe283485e9e" + integrity sha512-zQzDu8yO4DInw14FfuZVkIqsoQ9xJ+nbRjJug1iag+FBJCb598DnBZVpFvZdsOsRKbCtImhHhjK/CcImq1rTpA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/link" "^3.3.5" + "@react-aria/utils" "^3.14.1" + "@react-types/breadcrumbs" "^3.4.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/button@^3.6.3": + version "3.6.3" + resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.6.3.tgz#058f3f7ef935395ae2c744d4b926236b2e5c406c" + integrity sha512-t6UHFPFMlAQW0yefjhqwyQya6RYlUtMRtMKZjnRANbK6JskazkkLvu//qULs+vsiM21PLhspKVLcGdS+t2khsA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/toggle" "^3.4.3" + "@react-types/button" "^3.7.0" + "@react-types/shared" "^3.16.0" + +"@react-aria/calendar@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@react-aria/calendar/-/calendar-3.0.4.tgz#f97318ccfdf57294b66e71f751a3df20832f4b31" + integrity sha512-FE52NSR631YO+ew0k/Qt90C+qV9qJV53uAkFYWXzYE0zeE9/+IM0Jtrb/rdcmUhsx/c4l9tzNPAMdFjKvkawXw== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/date" "^3.0.1" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/utils" "^3.14.1" + "@react-stately/calendar" "^3.0.4" + "@react-types/button" "^3.7.0" + "@react-types/calendar" "^3.0.4" + "@react-types/shared" "^3.16.0" + +"@react-aria/checkbox@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.7.0.tgz#74e58e66a06908c78109dbbcc359c5485c1c6463" + integrity sha512-CGVfBcCK3e8YwjPSgIMTQbMxbjTtsDy9xGkw/7iCMVIsHC7MfzO8Ny5qJJbQ2dVkNnSfIgQdtWikYGJN2QjeDw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/label" "^3.4.3" + "@react-aria/toggle" "^3.4.1" + "@react-aria/utils" "^3.14.1" + "@react-stately/checkbox" "^3.3.1" + "@react-stately/toggle" "^3.4.3" + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/combobox@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-aria/combobox/-/combobox-3.4.3.tgz#c930ce318c37b7f7fee5687352afa4de0fce4355" + integrity sha512-MrpxrpJOOIRKMKkFDxTzQFu6y31eL15IsMbTRttO0NsrnQiJl19ojz6MpnhIJjnaC/Wz2EEWqnUawQsJjAVxyQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/listbox" "^3.7.1" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/menu" "^3.7.0" + "@react-aria/overlays" "^3.12.0" + "@react-aria/selection" "^3.12.0" + "@react-aria/textfield" "^3.8.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/collections" "^3.5.0" + "@react-stately/combobox" "^3.3.0" + "@react-stately/layout" "^3.9.0" + "@react-types/button" "^3.7.0" + "@react-types/combobox" "^3.5.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/datepicker@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@react-aria/datepicker/-/datepicker-3.2.0.tgz#3905a888cfed7d71a9ef212fb72448eb6dbb0175" + integrity sha512-qsnAcKeWzmZadXtQPpmTN4TNO10Vq/TXSsf5PF+2MInsJQjEWTN8YFFgTbKVf7ksDwfDRSarTV0f+hZbi5scHA== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/date" "^3.0.1" + "@internationalized/number" "^3.1.1" + "@internationalized/string" "^3.0.0" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/spinbutton" "^3.2.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/datepicker" "^3.2.0" + "@react-types/button" "^3.7.0" + "@react-types/calendar" "^3.0.4" + "@react-types/datepicker" "^3.1.3" + "@react-types/dialog" "^3.4.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/dialog@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.4.1.tgz#373c389e21b9b9fcc6947ec092074daedd46030c" + integrity sha512-1P6zCn78OP9nv7/1i4QsysOKQ6gxHy9JUyOj0ixrR1jwYI/psCM2MwT9cfPjuj7pGQys6lsu4glSmZ82zARURQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/overlays" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/overlays" "^3.4.3" + "@react-types/dialog" "^3.4.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/dnd@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-aria/dnd/-/dnd-3.0.0.tgz#3217595756ded984a0f8aa9a26b3e5b8f01acd12" + integrity sha512-0sJp1tqqqp4FIN+VBPcImrBy0Tb+qMAIeQ4RmqQCdrwb+T1WDcroyUnYoTRPBatsHBClQMM4z9ETKrudKJqC0w== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/string" "^3.0.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/overlays" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-aria/visually-hidden" "^3.6.0" + "@react-stately/dnd" "^3.0.0" + "@react-types/button" "^3.7.0" + "@react-types/shared" "^3.16.0" + +"@react-aria/focus@^3.10.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.10.0.tgz#12d85d46f58590a915009e57bddb2d90b56f5836" + integrity sha512-idI7Etgh6y2BYi3X4d+EuUpzR7gPZ94Lf/0UNnVyMkDM9fzcdz/8DCBt0qKOff24HlaLE1rmREt0+iTR/qRgbA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + clsx "^1.1.1" + +"@react-aria/grid@^3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@react-aria/grid/-/grid-3.5.1.tgz#0c5c719985cf66501cd6ae8d34673185de70f358" + integrity sha512-eWwhG9wHb6l6poZSvnhoSSCpNy1kG3HxIpcbMaR2/qllbUYgZ4PASyx4N2TT/VqBUsxCSwC/WqcDka11U9d94w== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/grid" "^3.4.1" + "@react-stately/selection" "^3.11.1" + "@react-stately/virtualizer" "^3.4.0" + "@react-types/checkbox" "^3.4.1" + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/gridlist@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@react-aria/gridlist/-/gridlist-3.1.1.tgz#541d4b3ea9327ddddda8556a2d6d8bcc65387438" + integrity sha512-/2f4RYqPF1Jxz2Zl5uScGh8trS/N+cp3YbihjLX/3q/qwBj6El72lpJCeF6zkGAJQx+bt1Uew5YB0qt9uMYZng== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/grid" "^3.5.1" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/list" "^3.6.0" + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/i18n@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.6.2.tgz#74fa50f4b13ca7efe7738fd1960732a076ed049d" + integrity sha512-/G22mZQcISX6DcKLBn4j/X53y2SOnFfiD4wOEuY7sIZZDryktd+3I/QHukCnNlf0tKK3PdixQLvWa9Q1RqTSaw== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/date" "^3.0.1" + "@internationalized/message" "^3.0.9" + "@internationalized/number" "^3.1.1" + "@internationalized/string" "^3.0.0" + "@react-aria/ssr" "^3.4.0" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/interactions@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.13.0.tgz#897ee2b4a7751bcf22c716ceccfc1321f427a8f2" + integrity sha512-gbZL+qs+6FPitR/abAramth4lqz/drEzXwzIDF6p6WyajF805mjyAgZin1/3mQygSE5BwJNDU7jMUSGRvgFyTw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/label@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.4.3.tgz#7cc1821cffb0dba6c9a82c2c0fb5655ddca52ba6" + integrity sha512-g8NSHQKha6xOpR0cUQ6cmH/HwGJdebEbyy+c1I6VeW6me8lSF47xLnybnA6LBV4x9hJqkST6rfL/oPaBMCEKNA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/utils" "^3.14.1" + "@react-types/label" "^3.7.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/link@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-aria/link/-/link-3.3.5.tgz#5c22a6cbff0ce982e135971b932e34183d163dfb" + integrity sha512-BbyoJ73IAQcQMYWFxhV/zJWYFlx4Edprm6xfBZKMEBrEpUcvBwe/X3QsCQmRXZ8fJodMjQ9SdQPyue1yi8Ksrw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-types/link" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/listbox@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.7.1.tgz#10ded709334146fbeccc66437074c0e070cbf4ad" + integrity sha512-vKovd+u8F7jdcogZeDPtm89gn390cR0xpMbOoyPzbACOdST43SYexDXWV4Ww/M2YWkdJxT3jZ576NeifcfO2MA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/collections" "^3.5.0" + "@react-stately/list" "^3.6.0" + "@react-types/listbox" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/live-announcer@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@react-aria/live-announcer/-/live-announcer-3.1.1.tgz#40f340f6794fca42682fb308fe750ff56bf7c07f" + integrity sha512-e7b+dRh1SUTla42vzjdbhGYkeLD7E6wIYjYaHW9zZ37rBkSqLHUhTigh3eT3k5NxFlDD/uRxTYuwaFnWQgR+4g== + dependencies: + "@babel/runtime" "^7.6.2" + +"@react-aria/menu@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.7.0.tgz#1aa8a06c3b89dcc94d9451f84b47409aeb673733" + integrity sha512-dCSg67G3vEXOovZyaojZXvcq19MLqual6oTSJC9WhNS/SR0AuNPbwMbD34a/b1Je73ro5bzjIbmQPyt/i3XaCA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/overlays" "^3.12.0" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/collections" "^3.5.0" + "@react-stately/menu" "^3.4.3" + "@react-stately/tree" "^3.4.0" + "@react-types/button" "^3.7.0" + "@react-types/menu" "^3.7.3" + "@react-types/shared" "^3.16.0" + +"@react-aria/meter@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-aria/meter/-/meter-3.3.3.tgz#023091968fd5d9ba3b2dc06ad5fc16ba9f1a8618" + integrity sha512-6pe6Gl5e9yZsDkFsdpQNx9eAqSKIjwcOJ4/mLgTiCVgZWtWYuxprLAPiN7OyCnSYPfLp36wkIrMkk82xfBYb9Q== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/progress" "^3.3.3" + "@react-types/meter" "^3.2.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/numberfield@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-aria/numberfield/-/numberfield-3.3.3.tgz#6a4861eb43abb4d9fcb06de256ee1ecc6047b3f1" + integrity sha512-DxVhoD+RsgN385f2OsOg5J1RYo1yZt0AUfIJdHn7FDWYCxruUVmEhzy1ovDxpXkseK0Gh3IdkfHvOfgiqE+pXg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/spinbutton" "^3.2.0" + "@react-aria/textfield" "^3.8.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/numberfield" "^3.3.0" + "@react-types/button" "^3.7.0" + "@react-types/numberfield" "^3.3.5" + "@react-types/shared" "^3.16.0" + "@react-types/textfield" "^3.6.1" + +"@react-aria/overlays@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.12.0.tgz#66fc930a39771888123c6fd1b246fedd5e76ad89" + integrity sha512-jsGeLTB3W3S5Cf2zDTxh1ODTNkE69miFDOGMB0VLwS1GWDwDvytcTRpBKY9JBrxad+4u0x6evnah7IbJ61qNBA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/ssr" "^3.4.0" + "@react-aria/utils" "^3.14.1" + "@react-aria/visually-hidden" "^3.6.0" + "@react-stately/overlays" "^3.4.3" + "@react-types/button" "^3.7.0" + "@react-types/overlays" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/progress@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-aria/progress/-/progress-3.3.3.tgz#b79327b27ad367f9ffd7376ed505b2281efdfc9d" + integrity sha512-yRE9fBfbjSdyWHWeQ4HqEURAT8foa9drGCJIKnMUx08dEsPAXvdh9tvnAvr1kbJnDlZxVwhlbTyFCwB+E2Mfag== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/label" "^3.4.3" + "@react-aria/utils" "^3.14.1" + "@react-types/progress" "^3.2.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/radio@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.4.1.tgz#23ed38aac1fb5094c7652919d331ec77094ba41f" + integrity sha512-a1JFxFOiExX1ZRGBE31LW4dgc3VmW2v3upJ5snGQldC83o0XxqNavmOef+fMsIRV0AQA/mcxAJVNQ0n9SfIiUQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/utils" "^3.14.1" + "@react-stately/radio" "^3.6.1" + "@react-types/radio" "^3.3.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/searchfield@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-aria/searchfield/-/searchfield-3.4.3.tgz#7f897f9c6ef6b19291d43d629e41680d1a7835b5" + integrity sha512-8WISGEyXWyVKRql4oVc9T5eNx8jTUwDQy0+ZSO5qGXuiZtlyeTJdWMrHN8I4SUdWEoF9c7R0eLhl0Twefnjkiw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/textfield" "^3.8.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/searchfield" "^3.3.3" + "@react-types/button" "^3.7.0" + "@react-types/searchfield" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/select@^3.8.3": + version "3.8.3" + resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.8.3.tgz#6137fc28b4ccfc4f3761e2f7ecd67b24b2f005e0" + integrity sha512-EkbzbpSEkq0oSmFSeOJskjPzopqmKQ2VxsEaJHL8RebVdJiNxp5kSaBOaH1KxZI9DgrzHQNSRKYJaSJ1pUTfbw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/listbox" "^3.7.1" + "@react-aria/menu" "^3.7.0" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-aria/visually-hidden" "^3.6.0" + "@react-stately/select" "^3.3.3" + "@react-types/button" "^3.7.0" + "@react-types/select" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-aria/selection@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.12.0.tgz#895ced39795180094ca79882c54b71441f4466e7" + integrity sha512-Akzx5Faxw+sOZFXLCOw6OddDNFbP5Kho3EP6bYJfd2pzMkBc8/JemC/YDrtIuy8e9x6Je9HHSZqtKjwiEaXWog== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/collections" "^3.5.0" + "@react-stately/selection" "^3.11.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/separator@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@react-aria/separator/-/separator-3.2.5.tgz#4c004023eb8c31ac36ddacaf938d776c1e04c50c" + integrity sha512-WJhqvUqMxxs18Qn8kGIdx7NCe/yoHev6w0TCxxcZMf/crJKWdSunv3YpbcQW67loBTRo1093RqhacPtXoRzQvg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + +"@react-aria/slider@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.2.3.tgz#cc4c61c427e2df07fb4cecf2c6113f0a485bb498" + integrity sha512-y2Sx2YExcWcg15Hzhxhqccpylq5xm2RlswnhBxzwY+ms8ZR4MK6UNL64wbCmOBLxhzjgi5mTWSB+OmVCZk5H4A== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/utils" "^3.14.1" + "@react-stately/radio" "^3.6.1" + "@react-stately/slider" "^3.2.3" + "@react-types/radio" "^3.3.1" + "@react-types/shared" "^3.16.0" + "@react-types/slider" "^3.3.1" + +"@react-aria/spinbutton@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@react-aria/spinbutton/-/spinbutton-3.2.0.tgz#f1e12954c3ca20c298f71494e371cec99781de1a" + integrity sha512-6pbfC/uOz1k+D6NL7l/o855yr3hMBaiLdZpKdGu4N/vybnyS5ZcjX9Y1VswBZjYgvZ3Ojp8fSu/buZMU/zAISw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/utils" "^3.14.1" + "@react-types/button" "^3.7.0" + "@react-types/shared" "^3.16.0" + +"@react-aria/ssr@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.4.0.tgz#a2b9a170214f56e41d3c4c933d0d8fcffa07a12a" + integrity sha512-qzuGk14/fUyUAoW/EBwgFcuMkVNXJVGlezTgZ1HovpCZ+p9844E7MUFHE7CuzFzPEIkVeqhBNIoIu+VJJ8YCOA== + dependencies: + "@babel/runtime" "^7.6.2" + +"@react-aria/switch@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@react-aria/switch/-/switch-3.3.0.tgz#b68753d0917964bb7e84aaa50b63ab4ecc4f23a7" + integrity sha512-A/6G9HjZYPvCvaUbrghdCH0rkQfaNbayruQJ+PWGITZbxhYZAUUW7wkxvxLpf3iX2K5+UtNNThxlEMcplEkVrw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/toggle" "^3.4.1" + "@react-stately/toggle" "^3.4.3" + "@react-types/switch" "^3.2.5" + +"@react-aria/table@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-aria/table/-/table-3.6.0.tgz#8258a6f53e233bb0f5e2da71b5fd33d866c4f517" + integrity sha512-hwq+5iwXVSirmi9Lr0v5wDOv7uz7UD+BUNFXP5d9nknrAKzVYDfpuNpz/Bbhpczp9R89VRBcFvcKJ3cWhESYnw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/grid" "^3.5.1" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/live-announcer" "^3.1.1" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/table" "^3.6.0" + "@react-stately/virtualizer" "^3.4.0" + "@react-types/checkbox" "^3.4.1" + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + "@react-types/table" "^3.3.3" + +"@react-aria/tabs@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.3.3.tgz#b66852794c6d72cf66a6789d78f89c6b41523b3c" + integrity sha512-0GeArynZzWQuNXIp1DUexNdfFC0vnTLAhN9cd3ZJDc7jbAvwy5HB363ElYqfTqNgvrtMF1QTJo9tY6KmYWxLeg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/selection" "^3.12.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/list" "^3.6.0" + "@react-stately/tabs" "^3.2.3" + "@react-types/shared" "^3.16.0" + "@react-types/tabs" "^3.1.5" + +"@react-aria/textfield@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@react-aria/textfield/-/textfield-3.8.0.tgz#1bc1cd93af82861a789b1bc1c4cd7c1b549f564e" + integrity sha512-PRU8q1gK0auDMH1YekJScZ4EZMrLrL3QJEHMNDdp2GDQlVISbPeTRy2On20DXfiG8GlXAtCWj9BiZhK2OE71DQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/label" "^3.4.3" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + "@react-types/textfield" "^3.6.1" + +"@react-aria/toggle@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.4.1.tgz#d48381ed7ebcd7637cc5be7ba5a5323a6e91c658" + integrity sha512-oVcjqsqvvEXW25vm3F2gxF5Csz8vRNKeF7Kc5pxqLrBohqMausChul+/Zisx5qVB4TL0yO3ygjTGbEvfEYQ1qg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/toggle" "^3.4.3" + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + "@react-types/switch" "^3.2.5" + +"@react-aria/tooltip@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.3.3.tgz#02f64fffe5ab4bb9db28b73896770c74f0d4dc42" + integrity sha512-EF58SQ70KEfGJQErsELJh1dk3KUDrBFmCEHo6kD1fVEHCqUgdWLkz+TCfkiP8VgFoj4WoE8zSpl3MpgGOQr/Gg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/focus" "^3.10.0" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-stately/tooltip" "^3.2.3" + "@react-types/shared" "^3.16.0" + "@react-types/tooltip" "^3.2.5" + +"@react-aria/utils@^3.14.1": + version "3.14.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.14.1.tgz#36aeb077f758f1f325951b1e3376a905217edd84" + integrity sha512-+ynP0YlxN02MHVEBaeuTrIhBsfBYpfJn36pZm2t7ZEFbafH8DPaMGZ70ffYZXAESkWzRULXL3e79DheWOFI1qA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/ssr" "^3.4.0" + "@react-stately/utils" "^3.5.1" + "@react-types/shared" "^3.16.0" + clsx "^1.1.1" + +"@react-aria/visually-hidden@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.6.0.tgz#cc4dd9e648a5c8b6d8dfbd1f70d8672b36d3f1bc" + integrity sha512-W3Ix5wdlVzh2GY7dytqOAyLCXiHzk3S4jLKSaoiCwPJX9fHE5zMlZwahhDy27V0LXfjmdjBltbwyEZOq4G/Q0w== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + clsx "^1.1.1" + "@react-spring/animated@~9.5.2": version "9.5.2" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.5.2.tgz#42785b4f369d9715e9ee32c04b78483e7bb85489" @@ -2457,6 +3066,453 @@ "@react-spring/shared" "~9.5.2" "@react-spring/types" "~9.5.2" +"@react-stately/calendar@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.0.4.tgz#b6bd88b11064a1b020a99f7e225becc050a35665" + integrity sha512-KaytmQVRqEOoKuLDgrm8RzY7ZHJ24IlDirN4dZj1wBHYt7RkAtwgqyTF/eyhS6/VYegmPhu53GcsSk0I3W+xLQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/date" "^3.0.1" + "@react-stately/utils" "^3.5.1" + "@react-types/calendar" "^3.0.4" + "@react-types/datepicker" "^3.1.3" + "@react-types/shared" "^3.16.0" + +"@react-stately/checkbox@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.3.1.tgz#4a53f8e813161f5c149c51b844624982e82a9247" + integrity sha512-r2hL11GF9r2ztUFEhpiVgiXgE+W99tyL1Kt7rOiTZ8/aMBGWwBxOHAdHeqcWFeBgOztXuJsKiDu82necEG4xhA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/toggle" "^3.4.3" + "@react-stately/utils" "^3.5.1" + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/collections@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.5.0.tgz#01606d4aa12364cc4296cc036e77690e48ec818c" + integrity sha512-3BAMRjJqrka0IGvyK4m3WslqCeiEfQGx7YsXEIgIgMJoLpk6Fi1Eh4CI8coBnl/wcVLiIRMCIvxubwFRWTgzdg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-types/shared" "^3.16.0" + +"@react-stately/combobox@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@react-stately/combobox/-/combobox-3.3.0.tgz#ae3566f3c715dbd4bf826927dbaaacb42ae108f5" + integrity sha512-+9xQW6C4nMcx7M72P4vZdQECa9CqzALTM3HTNAXgdCmfEezhns/m4xGmn4hoN8iw39yYvU8Ffs80rgTFQ+/oFg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/list" "^3.6.0" + "@react-stately/menu" "^3.4.3" + "@react-stately/select" "^3.3.3" + "@react-stately/utils" "^3.5.1" + "@react-types/combobox" "^3.5.5" + "@react-types/shared" "^3.16.0" + +"@react-stately/datepicker@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.2.0.tgz#74ae91de4b8a24ddaefbd60b38a1851080c3e53f" + integrity sha512-isFB8jpeiig3vfstWKkaY0cdejG0XT47Q9jZJgsrsEqpMVFBmcvlQQQ3WNqP4yC5c7Mrs3tAscY7WtbPIkDQ4g== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/date" "^3.0.1" + "@internationalized/string" "^3.0.0" + "@react-stately/overlays" "^3.4.3" + "@react-stately/utils" "^3.5.1" + "@react-types/datepicker" "^3.1.3" + "@react-types/shared" "^3.16.0" + +"@react-stately/dnd@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-stately/dnd/-/dnd-3.0.0.tgz#533035fb1180605e24431d503417e38d9788d830" + integrity sha512-TI3BqheEm9fUhqrMm6RFY6q8DcWfC5O/LK+IgHpQgOBhL+Vk/EwvGnRice1xyMEQKbAXf04WOFiAjZqfurLshQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/selection" "^3.11.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/grid@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.4.1.tgz#b3f771d64141e3753e16b7ddba0affa6f22a927a" + integrity sha512-IRaqXUQGji87Q+pYYQKJYTuUtgAjoDQaMOlvpvB9HlyK5faXq0H1tJsYAeVYpH0synfzCnr7CN0J6kSTbeL1jA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/selection" "^3.11.1" + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + +"@react-stately/layout@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@react-stately/layout/-/layout-3.9.0.tgz#2a23ec29443ef8103b330a7a8bda07b19c6a03da" + integrity sha512-uFdK98hIspBV9/RMW/JJaViuWyISdcm5GFplB361JZkhDaYblzomvkoX5Y1dKO5uH/BOjdM2AB5vfCb21oKEhg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/virtualizer" "^3.4.0" + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + "@react-types/table" "^3.3.3" + +"@react-stately/list@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.6.0.tgz#b5e04d59b8d53974c199f19712eb02c7a138896e" + integrity sha512-sah2JAiqlSZhg1tQBSv9866LeAJISmosOFsOsVZPfyfAewuCksA+8OHrFtbKmMyzU5MbrmpbR8v2zZH7c1CLdg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/collections" "^3.5.0" + "@react-stately/selection" "^3.11.1" + "@react-stately/utils" "^3.5.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/menu@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.4.3.tgz#65bb3fe29634047d3f6a3024577d3535e00802ae" + integrity sha512-ZWym6XQSLaC5uFUTZl6+mreEgzc8EUG6ElcnvdXYcH4DWUfswhLxCi3IdnG0lusWEi4NcHbZ2prEUxpT8VKqrg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/overlays" "^3.4.3" + "@react-stately/utils" "^3.5.1" + "@react-types/menu" "^3.7.3" + "@react-types/shared" "^3.16.0" + +"@react-stately/numberfield@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@react-stately/numberfield/-/numberfield-3.3.0.tgz#e1926996772440ea3c9d65ca9b0b9d1914ee9409" + integrity sha512-UYw8KpLEG7F6U3lHvrqWLdyiWmEeYwvwLlUPErIy+/heoBUW22FRjTIfOANmvVQoeSmd8aGIBWbCVRrbjU6Q5A== + dependencies: + "@babel/runtime" "^7.6.2" + "@internationalized/number" "^3.1.1" + "@react-stately/utils" "^3.5.1" + "@react-types/numberfield" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-stately/overlays@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.4.3.tgz#2e935c404c0845ee7a7c6f001ff057d315161a16" + integrity sha512-WZCr3J8hj0cplQki1OVBR3MXg2l9V017h15Y2h+TNduWvnKH0yYOE/XfWviAT4KUP0LYoQfCnZ7XMHv+UI+8JA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/utils" "^3.5.1" + "@react-types/overlays" "^3.6.5" + +"@react-stately/radio@^3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.6.1.tgz#7d2d7ad94cc910a5647c196ce747c2f4a9a160b6" + integrity sha512-Hcg2qgvR7ekKMzVKeGby1FgMk3Sw4iDcEY/K1Y6j7UmGjM2HtQOq614tWQSQeGB25pp5I2jAWlparJeX0vY/oA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/utils" "^3.5.1" + "@react-types/radio" "^3.3.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/searchfield@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-stately/searchfield/-/searchfield-3.3.3.tgz#36888922e96258815c77140bc26419fe98c9d555" + integrity sha512-pQxvFP05gPU2pcm+RuKg5Q8TuYcQ+SpxRwX4i4awwL/wSZTG7VmFkQpOaQK5wU558UXydMnK3QfifmCBV7IN9A== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/utils" "^3.5.1" + "@react-types/searchfield" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-stately/select@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.3.3.tgz#d042c88a63e9d4c6718a45034ea27789cbd34819" + integrity sha512-HTKKwx5tq21G2r3Q0CVC5v2Amftj1+DvBlFSRIC9ZqWyxeQg//HotX0GpYHzEEyj5hB1GjBklKJ4UVejqNbb0w== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/collections" "^3.5.0" + "@react-stately/list" "^3.6.0" + "@react-stately/menu" "^3.4.3" + "@react-stately/selection" "^3.11.1" + "@react-stately/utils" "^3.5.1" + "@react-types/select" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-stately/selection@^3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.11.1.tgz#580145bade9aebb8395ebc2edabed422d84fde0a" + integrity sha512-UHB6/eH5NJ+Q70G+pmnxohHfR3bh0szT+lOlWPj7Mh76WPu9bu07IHKLEob6PSzyJ81h7+Ysk3hdIgS3TewGog== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/collections" "^3.5.0" + "@react-stately/utils" "^3.5.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/slider@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.2.3.tgz#36dcd2ccf07021ec770bbdebaa43c2fd4531884a" + integrity sha512-l5ezt0+Gq67QO/J5u6YX00mzahRrANSXK/wBx7TVeIxqOAPOG9zc8M8O9Pa5fZB6lYAVpHMbV/aqLSkyy8ImTg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/i18n" "^3.6.2" + "@react-aria/utils" "^3.14.1" + "@react-stately/utils" "^3.5.1" + "@react-types/shared" "^3.16.0" + "@react-types/slider" "^3.3.1" + +"@react-stately/table@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.6.0.tgz#f47041d14b2b803da33720b4b754f3af3c91954a" + integrity sha512-B6zamfI06j3+kxlMm1mgn+JaQv5CdXgYsMLo96nrU+XRbn2WzAikc2w+XHmfnqlKAcm+PtcDjrshDOCMioP2QA== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/collections" "^3.5.0" + "@react-stately/grid" "^3.4.1" + "@react-stately/selection" "^3.11.1" + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + "@react-types/table" "^3.3.3" + +"@react-stately/tabs@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.2.3.tgz#39b7e7bf7dfe544868be4c1593578587cff0d5a9" + integrity sha512-23GX5iBX1IPY1sD4nq8sTgCfaCt+P2nORYnBWA01+iZoUX/g3BG3+3S2SVL1J7esmcapGnXNapUa2zEbf3aFRg== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/list" "^3.6.0" + "@react-stately/utils" "^3.5.1" + "@react-types/tabs" "^3.1.5" + +"@react-stately/toggle@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.4.3.tgz#331942e70314f918f852ee679b8f668d98771801" + integrity sha512-HsJLMa5d9i6SWyDIahkJExkanXZek86//hirsgSU0IvY7YJx33Wek8UwHE5Vskp39DAOu18QMz2GrAngnUErYQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/utils" "^3.5.1" + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/tooltip@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.2.3.tgz#edcd70239b0b872753e6636085e53eafd023a61c" + integrity sha512-RWCcqn6iz1IcOXX+TiXBql2kI5hgDlf49DiGZJqSGmNQujX1FVZ1uqn9yHpdh+/TZPZ7JeMvQu3S5lA+x4ehPw== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/overlays" "^3.4.3" + "@react-stately/utils" "^3.5.1" + "@react-types/tooltip" "^3.2.5" + +"@react-stately/tree@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.4.0.tgz#e3985fcc4a6c4014a6cb28b146ff5f6903c7bd4c" + integrity sha512-MqxSABMzykwI6Wj1B7+jBcCoYc0b05CueRTQDyoL+PfVhnV0SzOH6P84UPD+FHlz8x3RG/2hTTmLr4A8McO2nQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-stately/collections" "^3.5.0" + "@react-stately/selection" "^3.11.1" + "@react-stately/utils" "^3.5.1" + "@react-types/shared" "^3.16.0" + +"@react-stately/utils@^3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.5.1.tgz#502de762e5d33e892347c5f58053674e06d3bc92" + integrity sha512-INeQ5Er2Jm+db8Py4upKBtgfzp3UYgwXYmbU/XJn49Xw27ktuimH9e37qP3bgHaReb5L3g8IrGs38tJUpnGPHA== + dependencies: + "@babel/runtime" "^7.6.2" + +"@react-stately/virtualizer@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@react-stately/virtualizer/-/virtualizer-3.4.0.tgz#939c19d869ccf0e3f7c0e62ecb2406a9fe128cd1" + integrity sha512-Yy5RKlt6W/1+qjJAVHxPJA0RgpN3KNHcSpnFHdus2OuEvylSXZ2kqwflj97Ao4XfNSpDIs4NQS/eOq+mpZlNqQ== + dependencies: + "@babel/runtime" "^7.6.2" + "@react-aria/utils" "^3.14.1" + "@react-types/shared" "^3.16.0" + +"@react-types/breadcrumbs@^3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@react-types/breadcrumbs/-/breadcrumbs-3.4.5.tgz#ea77c88af05497b93bdeedc21dbb98973bbd57a2" + integrity sha512-5DXV6qW6Orronu1D9Op903m+lGzPajzJnsW6ygEiv6kjRutY33gIl1ePoQKoBQzNimtFs3uE4YLOw7nLzry1qg== + dependencies: + "@react-types/link" "^3.3.5" + "@react-types/shared" "^3.16.0" + +"@react-types/button@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.7.0.tgz#774c043d8090a505e60fdf26f026d5f0cc968f0f" + integrity sha512-81BQO3QxSgF9PTXsVozNdNCKxBOB1lpbCWocV99dN1ws9s8uaYw8pmJJZ0LJKLiOsIECQ/3QrhQjmWTDW/qTug== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/calendar@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.0.4.tgz#294a2931f87e547fdd8261aa4524e5cc8524d5a4" + integrity sha512-0hKaaEil2XbdUESQe9Yg2uLVNvNcFHVzXN6KoQHGBPnpWlkwa24ufKiX27mAWOAoce0nEXlVBG1H9C/kwLMcMw== + dependencies: + "@internationalized/date" "^3.0.1" + "@react-types/shared" "^3.16.0" + +"@react-types/checkbox@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.4.1.tgz#75a78b3f21f4cc72d2382761ba4c326aefd699db" + integrity sha512-kDMpy9SntjGQ7x00m5zmW8GENPouOtyiDgiEDKsPXUr2iYqHsNtricqVyG9S9+6hqpzuu8BzTcvZamc/xYjzlg== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/combobox@^3.5.5": + version "3.5.5" + resolved "https://registry.yarnpkg.com/@react-types/combobox/-/combobox-3.5.5.tgz#13410106fc2df8e3d02d53a33e9d2a6f3f2f6b61" + integrity sha512-gpDo/NTQFd5IfCZoNnG16N4/JfvwXpZBNc15Kn7bF+NcpSDhDpI26BZN4mvK4lljKCheD4VrEl9/3PtImCg7cA== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/datepicker@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@react-types/datepicker/-/datepicker-3.1.3.tgz#0bfc1755ca0a7680b68d1f9281721f72b2dcf7c8" + integrity sha512-5ZsCU/quVXMCQd3T9yLYKOviSghBaSx/vqzJDsDGimyFRAxd4n95PRl8SjlGjVf6lR0WSihCbcXB/D+b8/RJIA== + dependencies: + "@internationalized/date" "^3.0.1" + "@react-types/overlays" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-types/dialog@^3.4.5": + version "3.4.5" + resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.4.5.tgz#a12c4e6d69dd7f098eb8b1534107ae6d970f734b" + integrity sha512-FkxZAYNRWkZVH5rjlw6qyQ/SpoGcYtNI/JQvn1H/xtZy/OJh2b2ERxGWv5x0RItGSeyATdSwFO1Qnf1Kl2K02A== + dependencies: + "@react-types/overlays" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-types/grid@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.1.5.tgz#b0efef48202b40aa05913f1fe5b05d80e7d26c15" + integrity sha512-KiEywsOJ+wdzLmJerAKEMADdvdItaLfhdo3bFfn1lgNUaKiNDJctDYWlhOYsRePf7MIrzoZuXEFnJj45jfpiOQ== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/label@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-types/label/-/label-3.7.1.tgz#ad4d3d7a6b5ea6aca70f89661d7c358cf2ab5f94" + integrity sha512-wFpdtjSDBWO4xQQGF57V3PqvVVyE9TPj9ELWLs1yzL09fpXosycuEl5d79RywVlC9aF9dQYUfES09q/DZhRhMQ== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/link@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-types/link/-/link-3.3.5.tgz#0482c03ccb3e703922b201cda582801024508145" + integrity sha512-wEeYXqzRPwEwU6AakiRfsPrkGxm2l0gjIc992FBmHPz6MWU8eSATTwzeyI668eRzNrQvOBMI7il6lXuxDm1ZLg== + dependencies: + "@react-aria/interactions" "^3.13.0" + "@react-types/shared" "^3.16.0" + +"@react-types/listbox@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.3.5.tgz#c2222e3f50fbf377ed20b2d16e761b9c09d7adc8" + integrity sha512-7SMRJWUi7ayzQ7SUPCXXwgI/Ua3vg0PPQOZFsmJ4/E8VG/xK82IV7BYSZiNjUQuGpVZJL0VPndt/RwIrQO4S3w== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/menu@^3.7.3": + version "3.7.3" + resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.7.3.tgz#beb8d0fb7f1e50254e2e7661dfbfa4bb38826dad" + integrity sha512-3Pax24I/FyNKBjKyNR4ePD8eZs35Th57HzJAVjamQg2fHEDRomg9GQ7fdmfGj72Dv3x3JRCoPYqhJ3L5R3kbzg== + dependencies: + "@react-types/overlays" "^3.6.5" + "@react-types/shared" "^3.16.0" + +"@react-types/meter@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@react-types/meter/-/meter-3.2.5.tgz#99a381808e98765e7b645721bcc2bff4b5d5f19e" + integrity sha512-pBrHoWRSwrfo3JtCCxoniSEd27Pokt20Fj4ZkJxjjDtLdcHOM4Z1JIKvOlcXMCV35iknrVu4veDHpmXolI+vAw== + dependencies: + "@react-types/progress" "^3.2.5" + "@react-types/shared" "^3.16.0" + +"@react-types/numberfield@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-types/numberfield/-/numberfield-3.3.5.tgz#423aced559f7431e88b7988bf7e2cb3870fcdb1c" + integrity sha512-qBhUSkahiIeTW5IvKvyfLtVHgzyqwKfuDIOlJQiBwgrOPR96X8KDDsOib4r5SFv0lhibv0gQ5L5ucXbmwLyQ8A== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/overlays@^3.6.5": + version "3.6.5" + resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.6.5.tgz#466b325d9be51f67beb98b7bec3fd9295c72efac" + integrity sha512-IeWcF+YTucCYYHagNh8fZLH6R4YUONO1VHY57WJyIHwMy0qgEaKSQCwq72VO1fQJ0ySZgOgm31FniOyKkg6+eQ== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/progress@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@react-types/progress/-/progress-3.2.5.tgz#71780e48402cb25813c8edd07ee6075cdd972488" + integrity sha512-pFSqaj6rlSdPqGHVErJ8G3RkIyYigoJ3EVozvhR9bcKkLlhnzJiFgOZl+k5u/ZKJOA+YHivIHJwg+Kl1sG0J6A== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/radio@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.3.1.tgz#688570ba9901d21850a16c2aaafed5dd83e09966" + integrity sha512-q/x0kMvBsu6mH4bIkp/Jjrm9ff5y/p3UR0V4CmQFI7604gQd2Dt1dZMU/2HV9x70r1JfWRrDeRrVjUHVfFL5Vg== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/searchfield@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-types/searchfield/-/searchfield-3.3.5.tgz#a1019b10e2052faf8bde1dc3be99e9113e361844" + integrity sha512-g0kefTbrpqh5Cbv7skvlWfcDnopwTdoe7muHRYkuhMYbGbr8ZeUrCXpWUwVXBq8M24soLSHLuRohaEnKcwpHhw== + dependencies: + "@react-types/shared" "^3.16.0" + "@react-types/textfield" "^3.6.1" + +"@react-types/select@^3.6.5": + version "3.6.5" + resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.6.5.tgz#798abf0073b39eef041952198a9e84eff0ce9edc" + integrity sha512-FDeSA7TYMNnhsbXREnD4dWRSu21T5M4BLy+J/5VgwDpr3IN9pzbvngK8a3jc8Yg2S3igKYLMLYfmcsx+yk7ohA== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/shared@^3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.16.0.tgz#cab7bf0376969d1773480ecb2d6da5aa91391db5" + integrity sha512-IQgU4oAEvMwylEvaTsr2XB1G/mAoMe1JFYLD6G78v++oAR9l8o9MQxZ0YSeANDkqTamb2gKezGoT1RxvSKjVxw== + +"@react-types/slider@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.3.1.tgz#0e6a8d0767b1ab94f8c32541d50aaa6d93683df4" + integrity sha512-CbEa1v1IcUJD7VrFhWyOOlT7VyQ5DHEf/pNMkvICOBLMAwnWxS+tnTiRFgA/EbvV/vp24ydeszHYtMvsyRONRw== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/switch@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.2.5.tgz#e1db722e8beeed846cfcf9de94cad81b4e0ead78" + integrity sha512-DlUL0Bz79SUTRje/i8m6qn4Ipn+q8QnyIkyJhkoHeH1R0YNude8xZrBPWbj3zfdddAGDFSF1NzP69q0xmNAcTQ== + dependencies: + "@react-types/checkbox" "^3.4.1" + "@react-types/shared" "^3.16.0" + +"@react-types/table@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.3.3.tgz#350d6a86ad0aceab3fdd33470b4bd1346777aaf4" + integrity sha512-rdY8PCzdqumVd6EFgN4NCoNRHdU4dVKH2oufr50TrAVPAz2KyoNXaGcDGe0q4RjQeTk+fc0sCvRZZdpMwHRVpQ== + dependencies: + "@react-types/grid" "^3.1.5" + "@react-types/shared" "^3.16.0" + +"@react-types/tabs@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.1.5.tgz#8676dd16e0dc4be2d4d1cc33bb89cc679ef93abe" + integrity sha512-YgWY8IajCDBZmBzR3eii0aW6+SjcAT/dmqDNmfIuVVnDN7sHQ3PFa0nbmByvb0SfjOkJYumt8TJwFUCugohS8A== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/textfield@^3.6.1": + version "3.6.1" + resolved "https://registry.yarnpkg.com/@react-types/textfield/-/textfield-3.6.1.tgz#70494412144ddbe4e2ae37ba8ef63922e2a9f413" + integrity sha512-V3EyYw82GVJQbNN0OAWpOLs/UQij+AgUuJpxh8192p/q0B3/9lqepZ9b+Qts2XgMsA+3Db+KgFMWm2IdjaZbpQ== + dependencies: + "@react-types/shared" "^3.16.0" + +"@react-types/tooltip@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.2.5.tgz#f2940d3edbcf846dc15f9222f0162664641f183c" + integrity sha512-D4lN32JwQuA3JbCgcI26mgCkLHIj1WE8MTzf1McaasPkx7gVaqW+wfPyFwt99/Oo52TLvA/1oin78qePP67PSw== + dependencies: + "@react-types/overlays" "^3.6.5" + "@react-types/shared" "^3.16.0" + "@rushstack/eslint-patch@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.3.tgz#6801033be7ff87a6b7cadaf5b337c9f366a3c4b0" @@ -4417,6 +5473,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + cmd-shim@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-5.0.0.tgz#8d0aaa1a6b0708630694c4dbde070ed94c707724" @@ -7151,6 +8212,16 @@ intl-messageformat@10.1.1: "@formatjs/icu-messageformat-parser" "2.1.4" tslib "2.4.0" +intl-messageformat@^10.1.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.2.2.tgz#8436a6bf36d5d336513ff027fb099ca0d6676d13" + integrity sha512-iiaDjsEZNe92Vb8UIf46hT/3uVdcrL4x4GLjwFSVz/uC6ancQDUtyLVETX13wyTw78kBo3ONBMgiHoCtWN8ioQ== + dependencies: + "@formatjs/ecma402-abstract" "1.14.0" + "@formatjs/fast-memoize" "1.2.6" + "@formatjs/icu-messageformat-parser" "2.1.11" + tslib "2.4.0" + intl@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" @@ -10289,6 +11360,46 @@ react-animate-height@2.1.2: classnames "^2.2.5" prop-types "^15.6.1" +react-aria@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.21.0.tgz#429cf5ae13bbdf49cc2687254f5325a0cf386d82" + integrity sha512-gPzUZ+TxY8lDN1j4K90O3SVWBF1k870NuIePjgiymQqmKTMBGvBB6AswxSgbefakQjkgg+GsyQYGhoQMTtpcMA== + dependencies: + "@react-aria/breadcrumbs" "^3.4.0" + "@react-aria/button" "^3.6.3" + "@react-aria/calendar" "^3.0.4" + "@react-aria/checkbox" "^3.7.0" + "@react-aria/combobox" "^3.4.3" + "@react-aria/datepicker" "^3.2.0" + "@react-aria/dialog" "^3.4.1" + "@react-aria/dnd" "^3.0.0" + "@react-aria/focus" "^3.10.0" + "@react-aria/gridlist" "^3.1.1" + "@react-aria/i18n" "^3.6.2" + "@react-aria/interactions" "^3.13.0" + "@react-aria/label" "^3.4.3" + "@react-aria/link" "^3.3.5" + "@react-aria/listbox" "^3.7.1" + "@react-aria/menu" "^3.7.0" + "@react-aria/meter" "^3.3.3" + "@react-aria/numberfield" "^3.3.3" + "@react-aria/overlays" "^3.12.0" + "@react-aria/progress" "^3.3.3" + "@react-aria/radio" "^3.4.1" + "@react-aria/searchfield" "^3.4.3" + "@react-aria/select" "^3.8.3" + "@react-aria/selection" "^3.12.0" + "@react-aria/separator" "^3.2.5" + "@react-aria/slider" "^3.2.3" + "@react-aria/ssr" "^3.4.0" + "@react-aria/switch" "^3.3.0" + "@react-aria/table" "^3.6.0" + "@react-aria/tabs" "^3.3.3" + "@react-aria/textfield" "^3.8.0" + "@react-aria/tooltip" "^3.3.3" + "@react-aria/utils" "^3.14.1" + "@react-aria/visually-hidden" "^3.6.0" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -10588,6 +11699,11 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"