From 626099a2c98fb30d0cb53d8ccf79a6bf75a00059 Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 15 Sep 2020 14:12:36 +0900 Subject: [PATCH] feat(frontend): modal component and basic request hookup (#91) --- package.json | 2 + .../{PrimaryButton => Button}/index.tsx | 11 +- src/components/Common/Modal/index.tsx | 143 ++++++++++++++++++ src/components/Discover/index.tsx | 42 ++--- src/components/Search/index.tsx | 2 + src/components/TitleCard/index.tsx | 61 +++++++- src/hooks/useLockBodyScroll.ts | 28 ++++ src/hooks/useUser.ts | 19 +++ yarn.lock | 19 ++- 9 files changed, 298 insertions(+), 29 deletions(-) rename src/components/Common/{PrimaryButton => Button}/index.tsx (84%) create mode 100644 src/components/Common/Modal/index.tsx create mode 100644 src/hooks/useLockBodyScroll.ts diff --git a/package.json b/package.json index f7fc1a36e..a10748200 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "next": "9.5.3", "react": "16.13.1", "react-dom": "16.13.1", + "react-spring": "^8.0.27", "react-transition-group": "^4.4.1", "reflect-metadata": "^0.1.13", "sqlite3": "^5.0.0", @@ -43,6 +44,7 @@ "@types/lodash": "^4.14.161", "@types/node": "^14.10.0", "@types/react": "^16.9.49", + "@types/react-dom": "^16.9.8", "@types/react-transition-group": "^4.4.0", "@types/swagger-ui-express": "^4.1.2", "@types/yamljs": "^0.2.31", diff --git a/src/components/Common/PrimaryButton/index.tsx b/src/components/Common/Button/index.tsx similarity index 84% rename from src/components/Common/PrimaryButton/index.tsx rename to src/components/Common/Button/index.tsx index 68e330e4b..597ca089e 100644 --- a/src/components/Common/PrimaryButton/index.tsx +++ b/src/components/Common/Button/index.tsx @@ -1,7 +1,14 @@ import React, { ButtonHTMLAttributes } from 'react'; +export type ButtonType = + | 'default' + | 'primary' + | 'danger' + | 'warning' + | 'success'; + interface ButtonProps extends ButtonHTMLAttributes { - buttonType?: 'default' | 'primary' | 'danger' | 'warning' | 'success'; + buttonType?: ButtonType; buttonSize?: 'default' | 'lg' | 'md' | 'sm'; } @@ -38,7 +45,7 @@ const Button: React.FC = ({ break; default: buttonStyle.push( - 'border-gray-300 leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50' + 'leading-5 font-medium rounded-md text-gray-200 bg-cool-gray-500 hover:bg-cool-gray-400 hover:text-white focus:border-blue-300 focus:shadow-outline-blue active:text-gray-200 active:bg-cool-gray-400' ); } diff --git a/src/components/Common/Modal/index.tsx b/src/components/Common/Modal/index.tsx new file mode 100644 index 000000000..4901800a9 --- /dev/null +++ b/src/components/Common/Modal/index.tsx @@ -0,0 +1,143 @@ +import React, { MouseEvent, ReactNode } from 'react'; +import ReactDOM from 'react-dom'; +import Button, { ButtonType } from '../Button'; +import { useTransition, animated } from 'react-spring'; +import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll'; + +interface ModalProps { + title?: string; + onCancel?: (e: MouseEvent) => void; + onOk?: (e: MouseEvent) => void; + cancelText?: string; + okText?: string; + cancelButtonType?: ButtonType; + okButtonType?: ButtonType; + visible?: boolean; + disableScrollLock?: boolean; + backgroundClickable?: boolean; + iconSvg?: ReactNode; +} + +const Modal: React.FC = ({ + title, + onCancel, + onOk, + cancelText, + okText, + cancelButtonType, + okButtonType, + children, + visible, + disableScrollLock, + backgroundClickable = true, + iconSvg, +}) => { + useLockBodyScroll(!!visible, disableScrollLock); + const transitions = useTransition(visible, null, { + from: { opacity: 0 }, + enter: { opacity: 1 }, + leave: { opacity: 0 }, + config: { tension: 500, velocity: 40, friction: 60 }, + }); + const containerTransitions = useTransition(visible, null, { + from: { opacity: 0, transform: 'scale(0.5)' }, + enter: { opacity: 1, transform: 'scale(1)' }, + leave: { opacity: 0, transform: 'scale(0.5)' }, + config: { tension: 500, velocity: 40, friction: 60 }, + }); + + const cancelType = cancelButtonType ?? 'default'; + const okType = okButtonType ?? 'primary'; + + return ( + <> + {transitions.map( + ({ props, item, key }) => + item && + ReactDOM.createPortal( + { + if (e.key === 'Escape') { + typeof onCancel === 'function' && backgroundClickable + ? onCancel + : undefined; + } + }} + > + {containerTransitions.map( + ({ props, item, key }) => + item && ( + +
+ {iconSvg && ( +
+ {iconSvg} +
+ )} +
+ {title && ( + + )} + {children && ( +
+

+ {children} +

+
+ )} +
+
+ {(onCancel || onOk) && ( +
+ {typeof onOk === 'function' && ( + + )} + {typeof onCancel === 'function' && ( + + )} +
+ )} +
+ ) + )} +
, + document.body + ) + )} + + ); +}; + +export default Modal; diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 21bb5d717..9ca32f7f1 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -1,8 +1,7 @@ -import React, { useRef } from 'react'; -import useSWR, { useSWRInfinite } from 'swr'; +import React from 'react'; +import useSWR from 'swr'; import type { MovieResult, TvResult } from '../../../server/models/Search'; import TitleCard from '../TitleCard'; -import useVerticalScroll from '../../hooks/useVerticalScroll'; interface MovieDiscoverResult { page: number; @@ -18,17 +17,6 @@ interface TvDiscoverResult { results: TvResult[]; } -const getKey = ( - pageIndex: number, - previousPageData: MovieDiscoverResult | null -) => { - if (previousPageData && pageIndex + 1 > previousPageData.totalPages) { - return null; - } - - return `/api/v1/discover/movies?page=${pageIndex + 1}`; -}; - const Discover: React.FC = () => { const { data: movieData, error: movieError } = useSWR( '/api/v1/discover/movies' @@ -47,12 +35,16 @@ const Discover: React.FC = () => {
{movieData?.results.map((title) => ( -
+
{ {!movieData && !movieError && [...Array(10)].map((_item, i) => ( -
+
))} @@ -79,12 +74,16 @@ const Discover: React.FC = () => {
{tvData?.results.map((title) => ( -
+
{ {!tvData && !tvError && [...Array(10)].map((_item, i) => ( -
+
))} diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index cd0b65a86..f5414011f 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -89,6 +89,7 @@ const Search: React.FC = () => { case 'movie': titleCard = ( { case 'tv': titleCard = ( = ({ + id, image, summary, year, @@ -32,7 +38,21 @@ const TitleCard: React.FC = ({ status, mediaType, }) => { + const [currentStatus, setCurrentStatus] = useState(status); + const { hasPermission } = useUser(); const [showDetail, setShowDetail] = useState(false); + const [showRequestModal, setShowRequestModal] = useState(false); + + const request = async () => { + const response = await axios.post('/api/v1/request', { + mediaId: id, + mediaType, + }); + + if (response.data) { + setCurrentStatus(response.data.status); + } + }; // Just to get the year from the date if (year) { @@ -46,6 +66,34 @@ const TitleCard: React.FC = ({ height: 270, }} > + setShowRequestModal(false)} + onOk={() => request()} + title={`Request ${title}`} + okText="Request" + iconSvg={ + + + + } + > + {hasPermission(Permission.MANAGE_REQUESTS) + ? 'Your request will be immediately approved. Do you wish to continue?' + : undefined} +
= ({ right: '-1px', }} > - {status === MediaRequestStatus.AVAILABLE && ( + {currentStatus === MediaRequestStatus.AVAILABLE && ( )} - {status === MediaRequestStatus.PENDING && ( + {currentStatus === MediaRequestStatus.PENDING && ( )} - {status === MediaRequestStatus.APPROVED && ( + {currentStatus === MediaRequestStatus.APPROVED && ( )}
= ({ /> -