From e8776fd3361c6a579fba554b3b9ae8015f726f26 Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 15 Dec 2020 15:49:59 +0000 Subject: [PATCH] refactor(frontend): titlecard behavior changed to allow clicking anywhere to go through to title mobile behavior remains mostly the same, except after the first click, a second click anywhere else will go through to the title. --- src/components/TitleCard/index.tsx | 296 +++++++++++++++-------------- src/context/InteractionContext.tsx | 20 ++ src/hooks/useInteraction.ts | 78 ++++++++ src/hooks/useIsTouch.ts | 7 + src/pages/_app.tsx | 15 +- 5 files changed, 267 insertions(+), 149 deletions(-) create mode 100644 src/context/InteractionContext.tsx create mode 100644 src/hooks/useInteraction.ts create mode 100644 src/hooks/useIsTouch.ts diff --git a/src/components/TitleCard/index.tsx b/src/components/TitleCard/index.tsx index 5202c192..443f06a1 100644 --- a/src/components/TitleCard/index.tsx +++ b/src/components/TitleCard/index.tsx @@ -10,10 +10,11 @@ import Link from 'next/link'; import { MediaStatus } from '../../../server/constants/media'; import RequestModal from '../RequestModal'; import { defineMessages, useIntl } from 'react-intl'; +import { useIsTouch } from '../../hooks/useIsTouch'; const messages = defineMessages({ - movie: 'MOVIE', - tvshow: 'SERIES', + movie: 'Movie', + tvshow: 'Series', }); interface TitleCardProps { @@ -38,6 +39,7 @@ const TitleCard: React.FC = ({ mediaType, canExpand = false, }) => { + const isTouch = useIsTouch(); const intl = useIntl(); const [isUpdating, setIsUpdating] = useState(false); const [currentStatus, setCurrentStatus] = useState(status); @@ -74,9 +76,13 @@ const TitleCard: React.FC = ({
{ + if (!isTouch) { + setShowDetail(true); + } }} - onMouseEnter={() => setShowDetail(true)} onMouseLeave={() => setShowDetail(false)} onClick={() => setShowDetail(true)} onKeyDown={(e) => { @@ -146,158 +152,162 @@ const TitleCard: React.FC = ({ -
-
-
-
{year}
+ + +
+
+
{year}
-

- {title} -

-
- {summary} +

+ {title} +

+
+ {summary} +
-
-
- - - - - - - - - {(!currentStatus || - currentStatus === MediaStatus.UNKNOWN) && ( - - )} - {currentStatus === MediaStatus.PENDING && ( - + )} + {currentStatus === MediaStatus.PENDING && ( + - )} - {currentStatus === MediaStatus.PROCESSING && ( - + )} + {currentStatus === MediaStatus.PROCESSING && ( + - )} - {(currentStatus === MediaStatus.AVAILABLE || - currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && ( - + )} + {(currentStatus === MediaStatus.AVAILABLE || + currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && ( + - )} + + + + + )} +
-
-
+ +
diff --git a/src/context/InteractionContext.tsx b/src/context/InteractionContext.tsx new file mode 100644 index 00000000..0a78cdac --- /dev/null +++ b/src/context/InteractionContext.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import useInteraction from '../hooks/useInteraction'; + +interface InteractionContextProps { + isTouch: boolean; +} + +export const InteractionContext = React.createContext({ + isTouch: false, +}); + +export const InteractionProvider: React.FC = ({ children }) => { + const isTouch = useInteraction(); + + return ( + + {children} + + ); +}; diff --git a/src/hooks/useInteraction.ts b/src/hooks/useInteraction.ts new file mode 100644 index 00000000..61d4f99f --- /dev/null +++ b/src/hooks/useInteraction.ts @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; + +export const INTERACTION_TYPE = { + MOUSE: 'mouse', + PEN: 'pen', + TOUCH: 'touch', +}; + +const UPDATE_INTERVAL = 1000; // Throttle updates to the type to prevent flip flopping + +const useInteraction = (): boolean => { + const [isTouch, setIsTouch] = useState(false); + + useEffect(() => { + const hasTapEvent = 'ontouchstart' in window; + setIsTouch(hasTapEvent); + + let localTouch = hasTapEvent; + let lastTouchUpdate = Date.now(); + + const shouldUpdate = (): boolean => + lastTouchUpdate + UPDATE_INTERVAL < Date.now(); + + const onMouseMove = (): void => { + if (localTouch && shouldUpdate()) { + setTimeout(() => { + if (shouldUpdate()) { + setIsTouch(false); + localTouch = false; + } + }, UPDATE_INTERVAL); + } + }; + + const onTouchStart = (): void => { + lastTouchUpdate = Date.now(); + + if (!localTouch) { + setIsTouch(true); + localTouch = true; + } + }; + + const onPointerMove = (e: PointerEvent): void => { + const { pointerType } = e; + + switch (pointerType) { + case INTERACTION_TYPE.TOUCH: + case INTERACTION_TYPE.PEN: + return onTouchStart(); + default: + return onMouseMove(); + } + }; + + if (hasTapEvent) { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('touchstart', onTouchStart); + } else { + window.addEventListener('pointerdown', onPointerMove); + window.addEventListener('pointermove', onPointerMove); + } + + return () => { + if (hasTapEvent) { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('touchstart', onTouchStart); + } else { + window.removeEventListener('pointerdown', onPointerMove); + window.removeEventListener('pointermove', onPointerMove); + } + }; + }, []); + + return isTouch; +}; + +export default useInteraction; diff --git a/src/hooks/useIsTouch.ts b/src/hooks/useIsTouch.ts new file mode 100644 index 00000000..2f5ac983 --- /dev/null +++ b/src/hooks/useIsTouch.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react'; +import { InteractionContext } from '../context/InteractionContext'; + +export const useIsTouch = (): boolean => { + const { isTouch } = useContext(InteractionContext); + return isTouch; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 139813c2..53d15d7c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -12,6 +12,7 @@ import { IntlProvider } from 'react-intl'; import { LanguageContext, AvailableLocales } from '../context/LanguageContext'; import Head from 'next/head'; import Toast from '../components/Toast'; +import { InteractionProvider } from '../context/InteractionContext'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const loadLocaleData = (locale: string): Promise => { @@ -90,12 +91,14 @@ const CoreApp: Omit = ({ defaultLocale="en" messages={loadedMessages} > - - - Overseerr - - {component} - + + + + Overseerr + + {component} + +