diff --git a/components/mobile/MobileTradePage.tsx b/components/mobile/MobileTradePage.tsx index 317d7229..4b75ebed 100644 --- a/components/mobile/MobileTradePage.tsx +++ b/components/mobile/MobileTradePage.tsx @@ -82,7 +82,7 @@ const MobileTradePage = () => { diff --git a/components/mobile/SwipeableTabs.tsx b/components/mobile/SwipeableTabs.tsx index 0e1bc24b..dd965432 100644 --- a/components/mobile/SwipeableTabs.tsx +++ b/components/mobile/SwipeableTabs.tsx @@ -1,38 +1,217 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline' +import useDrag from 'hooks/useDrag' import { useTranslation } from 'next-i18next' +import React, { useContext, useEffect, useState } from 'react' +import { + ScrollMenu, + VisibilityContext, + getItemsPos, + slidingWindow, +} from 'react-horizontal-scrolling-menu' -const SwipeableTabs = ({ onChange, tabs, tabIndex }) => { - const { t } = useTranslation('common') +type scrollVisibilityApiType = React.ContextType + +function SwipeableTabs({ items, onChange, tabIndex }) { + const { dragStart, dragStop, dragMove, dragging } = useDrag() + + const handleDrag = + ({ scrollContainer }: scrollVisibilityApiType) => + (ev: React.MouseEvent) => + dragMove(ev, (posDiff) => { + if (scrollContainer.current) { + scrollContainer.current.scrollLeft += posDiff + } + }) + + const handleItemClick = + (itemId: string) => + ({ getItemById, scrollToItem }: scrollVisibilityApiType) => { + if (dragging) { + return false + } + onChange(parseInt(itemId)) + scrollToItem(getItemById(itemId), 'smooth', 'center', 'nearest') + } return ( -
-
- + +
+ ) +} +export default SwipeableTabs + +function onWheel( + { getItemById, items, visibleItems, scrollToItem }: scrollVisibilityApiType, + ev: React.WheelEvent +): void { + const isThouchpad = Math.abs(ev.deltaX) !== 0 || Math.abs(ev.deltaY) < 15 + + if (isThouchpad) { + ev.stopPropagation() + return + } + + if (ev.deltaY < 0) { + const nextGroupItems = slidingWindow( + items.toItemsKeys(), + visibleItems + ).next() + const { center } = getItemsPos(nextGroupItems) + scrollToItem(getItemById(center), 'smooth', 'center') + } else if (ev.deltaY > 0) { + const prevGroupItems = slidingWindow( + items.toItemsKeys(), + visibleItems + ).prev() + const { center } = getItemsPos(prevGroupItems) + scrollToItem(getItemById(center), 'smooth', 'center') + } +} + +function Card({ + selected, + onClick, + title, +}: { + itemId: string + selected: boolean + onClick: (x) => void + title: string +}) { + const { t } = useTranslation('common') + const visibility = React.useContext(VisibilityContext) + + useEffect(() => { + if (selected) { + onClick(visibility) + } + }, [selected]) + + return ( +
onClick(visibility)} + role="button" + tabIndex={0} + className={`flex h-10 w-32 items-center justify-center ${ + selected + ? 'border-b-2 border-th-primary text-th-primary' + : 'border-b-2 border-transparent text-th-fgd-3' + }`} + > + {t(title)}
) } -export default SwipeableTabs +function Arrow({ + children, + disabled, + onClick, + className, +}: { + children: React.ReactNode + disabled: boolean + onClick: VoidFunction + className?: string +}) { + return ( + + ) +} + +function LeftArrow() { + const { + isFirstItemVisible, + scrollPrev, + visibleItemsWithoutSeparators, + initComplete, + } = useContext(VisibilityContext) + + const [disabled, setDisabled] = useState( + !initComplete || (initComplete && isFirstItemVisible) + ) + useEffect(() => { + if (visibleItemsWithoutSeparators.length) { + setDisabled(isFirstItemVisible) + } + }, [isFirstItemVisible, visibleItemsWithoutSeparators]) + + return ( + scrollPrev()} className="-left-2"> + + + ) +} + +function RightArrow() { + const { isLastItemVisible, scrollNext, visibleItemsWithoutSeparators } = + useContext(VisibilityContext) + + const [disabled, setDisabled] = useState( + !visibleItemsWithoutSeparators.length && isLastItemVisible + ) + useEffect(() => { + if (visibleItemsWithoutSeparators.length) { + setDisabled(isLastItemVisible) + } + }, [isLastItemVisible, visibleItemsWithoutSeparators]) + + return ( + scrollNext()} + className="-right-2" + > + + + ) +} diff --git a/hooks/useDrag.tsx b/hooks/useDrag.tsx new file mode 100644 index 00000000..7f8d5936 --- /dev/null +++ b/hooks/useDrag.tsx @@ -0,0 +1,46 @@ +import React from 'react' + +export default function useDrag() { + const [clicked, setClicked] = React.useState(false) + const [dragging, setDragging] = React.useState(false) + const position = React.useRef(0) + + const dragStart = React.useCallback((ev: React.MouseEvent) => { + position.current = ev.clientX + setClicked(true) + }, []) + + const dragStop = React.useCallback( + () => + // NOTE: need some delay so item under cursor won't be clicked + window.requestAnimationFrame(() => { + setDragging(false) + setClicked(false) + }), + [] + ) + + const dragMove = (ev: React.MouseEvent, cb: (posDif: number) => void) => { + const newDiff = position.current - ev.clientX + + const movedEnough = Math.abs(newDiff) > 5 + + if (clicked && movedEnough) { + setDragging(true) + } + + if (dragging && movedEnough) { + position.current = ev.clientX + cb(newDiff) + } + } + + return { + dragStart, + dragStop, + dragMove, + dragging, + position, + setDragging, + } +} diff --git a/package.json b/package.json index fa200914..5d444f2c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-cool-dimensions": "^2.0.7", "react-dom": "^17.0.2", "react-grid-layout": "^1.3.3", + "react-horizontal-scrolling-menu": "^2.8.2", "react-nice-dates": "^3.1.0", "react-portal": "^4.2.1", "react-qr-code": "^2.0.3", diff --git a/pages/account.tsx b/pages/account.tsx index 48a109e4..50078904 100644 --- a/pages/account.tsx +++ b/pages/account.tsx @@ -57,6 +57,7 @@ import { handleWalletConnect } from 'components/ConnectWalletButton' import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup' import NftProfilePicModal from 'components/NftProfilePicModal' import ProfileImage from 'components/ProfileImage' +import SwipeableTabs from 'components/mobile/SwipeableTabs' export async function getStaticProps({ locale }) { return { @@ -448,40 +449,47 @@ export default function Account() { ) : null}
- {mangoAccount ? ( - - ) : null} {mangoAccount ? ( !isMobile ? ( - + <> + + + ) : ( - -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
+ <> + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ) ) : connected ? ( isLoading ? ( diff --git a/pages/markets.tsx b/pages/markets.tsx index f0414ca8..c7c3cf7d 100644 --- a/pages/markets.tsx +++ b/pages/markets.tsx @@ -54,7 +54,7 @@ export default function Markets() { ) : ( )} diff --git a/pages/stats.tsx b/pages/stats.tsx index 2b71a0ec..a6a0b261 100644 --- a/pages/stats.tsx +++ b/pages/stats.tsx @@ -62,7 +62,7 @@ export default function StatsPage() { ) : ( )} diff --git a/yarn.lock b/yarn.lock index cc0451a7..94ac0d9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3229,6 +3229,11 @@ commander@^8.0.0, commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +compute-scroll-into-view@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab" + integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5602,6 +5607,13 @@ react-grid-layout@^1.3.3: react-draggable "^4.0.0" react-resizable "^3.0.4" +react-horizontal-scrolling-menu@^2.8.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-2.8.2.tgz#cb7bc7c6798e0cd9203555cf4e6fcffb5f0ba0f2" + integrity sha512-OmCQDONx/4TnyxQr+RoxP7AwSPQxdQGkFnWuCknF3s2pySNAogL4AB1LQv6bUrXIwFMXdrJBqxFeq3tZ24J7/w== + dependencies: + smooth-scroll-into-view-if-needed "^1.1.32" + react-i18next@^11.15.5: version "11.16.2" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.16.2.tgz#650b18c12a624057ee2651ba4b4a989b526be554" @@ -6018,6 +6030,13 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scroll-into-view-if-needed@^2.2.28: + version "2.2.29" + resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885" + integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg== + dependencies: + compute-scroll-into-view "^1.0.17" + scrypt-js@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" @@ -6145,6 +6164,13 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" +smooth-scroll-into-view-if-needed@^1.1.32: + version "1.1.33" + resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.33.tgz#2c7b88c82784c69030cb0489b9df584e94e01533" + integrity sha512-crS8NfAaoPrtVYOCMSAnO2vHRgUp22NiiDgEQ7YiaAy5xe2jmR19Jm+QdL8+97gO8ENd7PUyQIAQojJyIiyRHw== + dependencies: + scroll-into-view-if-needed "^2.2.28" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"