Skip to content

Commit

Permalink
feat: discover inline customization (#3220)
Browse files Browse the repository at this point in the history
  • Loading branch information
sct committed Jan 6, 2023
1 parent 0d8b390 commit 8bd10b5
Show file tree
Hide file tree
Showing 19 changed files with 1,032 additions and 579 deletions.
62 changes: 37 additions & 25 deletions cypress/e2e/settings/discover-customization.cy.ts
Expand Up @@ -5,35 +5,42 @@ describe('Discover Customization', () => {
});

it('show the discover customization settings', () => {
cy.visit('/settings');
cy.visit('/');

cy.get('[data-testid=discover-start-editing]').click();

cy.get('[data-testid=discover-customization]')
.should('contain', 'Discover Customization')
cy.get('[data-testid=create-slider-header')
.should('contain', 'Create New Slider')
.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(
cy.get('[data-testid=discover-slider-edit-mode]').should(
'contain',
'Recently Added'
);
cy.get('[data-testid=discover-slider-edit-mode]').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.visit('/');

cy.get('[data-testid=discover-start-editing]').click();

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.trigger('dragstart', { dataTransfer });
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.trigger('drop', { dataTransfer });
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.trigger('dragend', { dataTransfer });

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.should('contain', 'Recently Added');

Expand All @@ -42,23 +49,25 @@ describe('Discover Customization', () => {

cy.reload();

cy.get('[data-testid=discover-start-editing]').click();

dataTransfer = new DataTransfer();

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.should('contain', 'Recently Added');

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.trigger('dragstart', { dataTransfer });
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.trigger('drop', { dataTransfer });
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.trigger('dragend', { dataTransfer });

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.eq(1)
.should('contain', 'Recent Requests');

Expand All @@ -67,10 +76,12 @@ describe('Discover Customization', () => {
});

it('can create a new discover option and remove it', () => {
cy.visit('/settings');
cy.visit('/');
cy.intercept('/api/v1/settings/discover/*').as('discoverSlider');
cy.intercept('/api/v1/search/keyword*').as('searchKeyword');

cy.get('[data-testid=discover-start-editing]').click();

const sliderTitle = 'Custom Keyword Slider';

cy.get('#sliderType').select('TMDB Movie Keyword');
Expand Down Expand Up @@ -98,14 +109,16 @@ describe('Discover Customization', () => {
cy.wait('@getDiscoverSliders');
cy.wait(1000);

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.should('contain', sliderTitle);

// Make sure its still there even if we reload
cy.reload();

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-start-editing]').click();

cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.should('contain', sliderTitle);

Expand All @@ -114,10 +127,10 @@ describe('Discover Customization', () => {

cy.get('.slider-header').should('not.contain', sliderTitle);

cy.visit('/settings');
cy.get('[data-testid=discover-start-editing]').click();

// Enable it, and check again
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.find('[role="checkbox"]')
.click();
Expand All @@ -131,20 +144,19 @@ describe('Discover Customization', () => {
.next('[data-testid=media-slider]')
.find('[data-testid=title-card]');

cy.visit('/settings');
cy.get('[data-testid=discover-start-editing]').click();

// let's delete it and confirm its deleted.
cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.find('button')
.should('contain', 'Remove')
.find('[data-testid=discover-slider-remove-button]')
.click();

cy.wait('@discoverSlider');
cy.wait('@getDiscoverSliders');
cy.wait(1000);

cy.get('[data-testid=discover-option]')
cy.get('[data-testid=discover-slider-edit-mode]')
.first()
.should('not.contain', sliderTitle);
});
Expand Down
52 changes: 52 additions & 0 deletions overseerr-api.yml
Expand Up @@ -26,6 +26,8 @@ tags:
description: Endpoints related to retrieving movies and their details.
- name: tv
description: Endpoints related to retrieving TV series and their details.
- name: keyword
description: Endpoints related to getting keywords and their details.
- name: person
description: Endpoints related to retrieving person details.
- name: media
Expand Down Expand Up @@ -3121,6 +3123,35 @@ paths:
items:
$ref: '#/components/schemas/DiscoverSlider'
/settings/discover/{sliderId}:
put:
summary: Update a single slider
description: |
Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission.
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: 'Slider Title'
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'
delete:
summary: Delete slider by ID
description: Deletes the slider with the provided sliderId. Requires the `ADMIN` permission.
Expand Down Expand Up @@ -6143,6 +6174,27 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Issue'
/keyword/{keywordId}:
get:
summary: Get keyword
description: |
Returns a single keyword in JSON format.
tags:
- keyword
parameters:
- in: path
name: keywordId
required: true
schema:
type: number
example: 1
responses:
'200':
description: Keyword returned
content:
application/json:
schema:
$ref: '#/components/schemas/Keyword'
security:
- cookieAuth: []
- apiKey: []
21 changes: 21 additions & 0 deletions server/routes/index.ts
Expand Up @@ -278,6 +278,27 @@ router.get('/backdrops', async (req, res, next) => {
}
});

router.get('/keyword/:keywordId', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage();

try {
const result = await tmdb.getKeywordDetails({
keywordId: Number(req.params.keywordId),
});

return res.status(200).json(result);
} catch (e) {
logger.debug('Something went wrong retrieving keyword data', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve keyword data.',
});
}
});

router.get('/', (_req, res) => {
return res.status(200).json({
api: 'Overseerr API',
Expand Down
31 changes: 31 additions & 0 deletions server/routes/settings/discover.ts
Expand Up @@ -77,6 +77,37 @@ discoverSettingRoutes.get('/reset', async (_req, res) => {
return res.status(204).send();
});

discoverSettingRoutes.put('/:sliderId', async (req, res, next) => {
const sliderRepository = getRepository(DiscoverSlider);

const slider = req.body as DiscoverSlider;

try {
const existingSlider = await sliderRepository.findOneOrFail({
where: {
id: Number(req.params.sliderId),
},
});

// 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);

return res.status(200).json(existingSlider);
} catch (e) {
logger.error('Something went wrong updating a slider.', {
label: 'API',
errorMessage: e.message,
});
next({ status: 404, message: 'Slider not found or cannot be updated.' });
}
});

discoverSettingRoutes.delete('/:sliderId', async (req, res, next) => {
const sliderRepository = getRepository(DiscoverSlider);

Expand Down
89 changes: 45 additions & 44 deletions src/components/Common/ConfirmButton/index.tsx
@@ -1,6 +1,6 @@
import Button from '@app/components/Common/Button';
import useClickOutside from '@app/hooks/useClickOutside';
import { useRef, useState } from 'react';
import { forwardRef, useRef, useState } from 'react';

interface ConfirmButtonProps {
onClick: () => void;
Expand All @@ -9,50 +9,51 @@ interface ConfirmButtonProps {
children: React.ReactNode;
}

const ConfirmButton = ({
onClick,
children,
confirmText,
className,
}: ConfirmButtonProps) => {
const ref = useRef(null);
useClickOutside(ref, () => setIsClicked(false));
const [isClicked, setIsClicked] = useState(false);
return (
<Button
buttonType="danger"
className={`relative overflow-hidden ${className}`}
onClick={(e) => {
e.preventDefault();
const ConfirmButton = forwardRef<HTMLButtonElement, ConfirmButtonProps>(
({ onClick, children, confirmText, className }, parentRef) => {
const ref = useRef(null);
useClickOutside(ref, () => setIsClicked(false));
const [isClicked, setIsClicked] = useState(false);
return (
<Button
ref={parentRef}
buttonType="danger"
className={`relative overflow-hidden ${className}`}
onClick={(e) => {
e.preventDefault();

if (!isClicked) {
setIsClicked(true);
} else {
onClick();
}
}}
>
&nbsp;
<div
ref={ref}
className={`absolute inset-0 flex h-full w-full transform-gpu items-center justify-center transition duration-300 ${
isClicked
? '-translate-y-full opacity-0'
: 'translate-y-0 opacity-100'
}`}
if (!isClicked) {
setIsClicked(true);
} else {
onClick();
}
}}
>
{children}
</div>
<div
ref={ref}
className={`absolute inset-0 flex h-full w-full transform-gpu items-center justify-center transition duration-300 ${
isClicked ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0'
}`}
>
{confirmText}
</div>
</Button>
);
};
<div
ref={ref}
className={`relative inset-0 flex h-full w-full transform-gpu items-center justify-center transition duration-300 ${
isClicked
? '-translate-y-full opacity-0'
: 'translate-y-0 opacity-100'
}`}
>
{children}
</div>
<div
ref={ref}
className={`absolute inset-0 flex h-full w-full transform-gpu items-center justify-center transition duration-300 ${
isClicked
? 'translate-y-0 opacity-100'
: 'translate-y-full opacity-0'
}`}
>
{confirmText}
</div>
</Button>
);
}
);

ConfirmButton.displayName = 'ConfirmButton';

export default ConfirmButton;

0 comments on commit 8bd10b5

Please sign in to comment.