diff --git a/package-lock.json b/package-lock.json index 1b906eb..97a328c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-apexcharts": "^1.5.0", "react-dom": "18.2.0", "react-intersection-observer": "^9.10.3", + "react-joyride": "^2.9.3", "sequelize": "^6.37.3", "sha256": "^0.2.0", "socket.io": "^4.6.1", @@ -2401,6 +2402,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@gilbarbara/deep-equal": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", + "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4223,7 +4230,6 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -4244,7 +4250,6 @@ "version": "18.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.16.tgz", "integrity": "sha512-LLFWr12ZhBJ4YVw7neWLe6Pk7Ey5R9OCydfuMsz1L8bZxzaawJj2p06Q8/EFEHDeTBQNFLF62X+CG7B2zIyu0Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4266,7 +4271,6 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", - "dev": true, "license": "MIT" }, "node_modules/@types/semver": { @@ -6315,6 +6319,12 @@ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "license": "MIT" }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6325,7 +6335,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8701,6 +8710,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz", + "integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==", + "license": "MIT" + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -10344,6 +10359,17 @@ "node": ">=12.13.0" } }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -11246,6 +11272,55 @@ "react": "^18.2.0" } }, + "node_modules/react-floater": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", + "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "is-lite": "^0.8.2", + "popper.js": "^1.16.0", + "prop-types": "^15.8.1", + "tree-changes": "^0.9.1" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/is-lite": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", + "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/tree-changes": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", + "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.1.1", + "is-lite": "^0.8.2" + } + }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, "node_modules/react-intersection-observer": { "version": "9.16.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", @@ -11267,6 +11342,41 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-joyride": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.3.tgz", + "integrity": "sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "deep-diff": "^1.0.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.7.9", + "react-innertext": "^1.1.5", + "react-is": "^16.13.1", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes": "^0.11.2", + "type-fest": "^4.27.0" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-joyride/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -11739,6 +11849,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/scroll": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz", + "integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==", + "license": "MIT" + }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", @@ -11748,6 +11864,12 @@ "compute-scroll-into-view": "^3.0.2" } }, + "node_modules/scrollparent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz", + "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==", + "license": "ISC" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -13015,6 +13137,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-changes": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.3.tgz", + "integrity": "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "is-lite": "^1.2.1" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index 1a84a9b..61da024 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-apexcharts": "^1.5.0", "react-dom": "18.2.0", "react-intersection-observer": "^9.10.3", + "react-joyride": "^2.9.3", "sequelize": "^6.37.3", "sha256": "^0.2.0", "socket.io": "^4.6.1", diff --git a/src/components/UI/GuideContent/index.tsx b/src/components/UI/GuideContent/index.tsx new file mode 100644 index 0000000..5e3b92f --- /dev/null +++ b/src/components/UI/GuideContent/index.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { GuideContentProps } from './types'; + +function GuideContent({ onEnter, text }: GuideContentProps) { + useEffect(() => { + if (onEnter) onEnter(); + }, [onEnter]); + + return

{text}

; +} + +export default GuideContent; diff --git a/src/components/UI/GuideContent/types.ts b/src/components/UI/GuideContent/types.ts new file mode 100644 index 0000000..adf3d1f --- /dev/null +++ b/src/components/UI/GuideContent/types.ts @@ -0,0 +1,4 @@ +export interface GuideContentProps { + text: string; + onEnter?: () => void; +} diff --git a/src/components/UI/GuideTooltip/index.tsx b/src/components/UI/GuideTooltip/index.tsx new file mode 100644 index 0000000..caa01b1 --- /dev/null +++ b/src/components/UI/GuideTooltip/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import type { TooltipRenderProps } from 'react-joyride'; +import styles from './styles.module.scss'; +import Button from '../Button/Button'; + +export default function GuideTooltip({ + index, + size, + step, + primaryProps, + skipProps, + tooltipProps, + isLastStep, +}: TooltipRenderProps) { + return ( +
+
+
+ {index + 1}/{size} +
+
+ {typeof step.content === 'string' ?

{step.content}

: step.content} +
+ + +
+ + {!isLastStep && ( + + )} +
+ ); +} diff --git a/src/components/UI/GuideTooltip/styles.module.scss b/src/components/UI/GuideTooltip/styles.module.scss new file mode 100644 index 0000000..bc18efb --- /dev/null +++ b/src/components/UI/GuideTooltip/styles.module.scss @@ -0,0 +1,77 @@ +.tooltip { + transition: none !important; + width: 300px; + + &__body { + width: 100%; + text-align: center; + border-radius: 10px; + background-color: #fff; + padding: 15px; + display: flex; + flex-direction: column; + gap: 10px; + } + + &__index { + font-size: 20px; + font-weight: 600; + color: #0c0c3a; + line-height: 100%; + } + + &__content, + &__content p { + color: #0c0c3a; + font-size: 14px; + font-weight: 400; + line-height: 140%; + } + + &__btn { + padding: 13px; + font-size: 14px; + font-weight: 500; + line-height: 100%; + } + + &__skip { + margin-top: 8px; + width: 100%; + cursor: pointer; + font-size: 14px; + font-weight: 500; + line-height: 140%; + color: var(--guide-skip-btn); + background-color: transparent !important; + } +} + +@media screen and (max-width:620px) { + .tooltip { + width: 200px; + + &__body { + gap: 8px; + padding: 10px; + } + + &__content, + &__content p { + font-size: 12px; + } + + &__btn, + &__skip { + font-size: 12px; + } + + &__btn { + padding: 10px; + } + + &__index { + font-size: 16px; + } + } +} \ No newline at end of file diff --git a/src/components/default/GuideRegistrator/index.tsx b/src/components/default/GuideRegistrator/index.tsx new file mode 100644 index 0000000..8e96994 --- /dev/null +++ b/src/components/default/GuideRegistrator/index.tsx @@ -0,0 +1,16 @@ +import { useTour } from '@/store/guide-provider'; +import { useEffect } from 'react'; +import type { Step } from 'react-joyride'; + +type Props = { name: string; steps: Step[]; autoStartOnceVersion?: string }; + +export default function GuideRegistrator({ name, steps, autoStartOnceVersion }: Props) { + const { register, startOnce } = useTour(name); + + useEffect(() => { + register(name, steps); + if (autoStartOnceVersion) startOnce(autoStartOnceVersion); + }, [name, steps, register, startOnce, autoStartOnceVersion]); + + return <>; +} diff --git a/src/components/dex/InputPanelItem/index.tsx b/src/components/dex/InputPanelItem/index.tsx index a4f8e86..950c601 100644 --- a/src/components/dex/InputPanelItem/index.tsx +++ b/src/components/dex/InputPanelItem/index.tsx @@ -51,9 +51,13 @@ function InputPanelItem(props: InputPanelItemProps) { const [hasImmediateMatch, setHasImmediateMatch] = useState(false); const isBuy = buySellState?.code === 'buy'; - function goToSuitableTab() { + function goToTab(name?: string) { const params = new URLSearchParams(searchParams.toString()); - params.set('tab', 'matches'); + if (name) { + params.set('tab', name); + } else { + params.delete('tab'); + } router.replace(`${pathname}?${params.toString()}`, undefined, { shallow: true, @@ -118,6 +122,8 @@ function InputPanelItem(props: InputPanelItemProps) { if (result.success) { if (result.data?.immediateMatch) { setHasImmediateMatch(true); + goToTab(); + scrollToOrderList(); } onAfter(); resetForm(); @@ -151,7 +157,7 @@ function InputPanelItem(props: InputPanelItemProps) { const showTotalError = priceState !== '' && amountState !== '' && !totalValid; return ( -
+
{hasImmediateMatch && ( { scrollToOrderList(); - goToSuitableTab(); + goToTab('matches'); setHasImmediateMatch(false); }} > diff --git a/src/components/dex/OrdersPool/index.tsx b/src/components/dex/OrdersPool/index.tsx index ff007af..3c78c18 100644 --- a/src/components/dex/OrdersPool/index.tsx +++ b/src/components/dex/OrdersPool/index.tsx @@ -263,7 +263,7 @@ const OrdersPool = (props: OrdersPoolProps) => { useMouseLeave(ordersInfoRef, () => setOrdersInfoTooltip(null)); return ( <> -
+
diff --git a/src/components/dex/TradingHeader/components/DexGuide/index.tsx b/src/components/dex/TradingHeader/components/DexGuide/index.tsx new file mode 100644 index 0000000..6dd75ed --- /dev/null +++ b/src/components/dex/TradingHeader/components/DexGuide/index.tsx @@ -0,0 +1,102 @@ +import GuideRegistrator from '@/components/default/GuideRegistrator'; +import GuideContent from '@/components/UI/GuideContent'; +import { useMediaQuery } from '@/hook/useMediaQuery'; +import { usePathname, useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { Placement, Step } from 'react-joyride'; + +const DexGuide = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + const isMobile = useMediaQuery(`(max-width: 620px)`); + + const getPlacement = (position: 'auto' | Placement | 'center' | undefined) => { + if (isMobile) return 'auto'; + + return position; + }; + + const changeTab = (name?: string) => { + const params = new URLSearchParams(searchParams.toString()); + + if (name) { + params.set('tab', name); + } else { + params.delete('tab'); + } + + const qs = params.toString(); + router.replace(qs ? `${pathname}?${qs}` : pathname, undefined, { + shallow: true, + scroll: false, + }); + }; + + const steps: Step[] = [ + { + target: '[data-tour="orders-pool"]', + placement: getPlacement('right-start'), + content: ( + + ), + disableBeacon: true, + }, + { + target: '[data-tour="input-panel"]', + placement: getPlacement('left-start'), + content: ( + + ), + }, + { + target: '[data-tour="user-orders"]', + placement: getPlacement('top-start'), + content: ( + changeTab()} + text="This tab shows all of your active orders. You can track price, quantity, and cancel them anytime." + /> + ), + }, + { + target: '[data-tour="user-orders"]', + placement: getPlacement('top-start'), + content: ( + changeTab('matches')} + text="Here you see orders that have been successfully matched and executed." + /> + ), + }, + { + target: '[data-tour="user-orders"]', + placement: getPlacement('top-start'), + content: ( + changeTab('requests')} + text="This section lists your pending requests that are waiting for approval or fulfillment." + /> + ), + }, + { + target: '[data-tour="user-orders"]', + placement: getPlacement('top-start'), + content: ( + changeTab('offers')} + text="In this tab you can find offers you’ve made to other traders that are not yet matched." + /> + ), + }, + ]; + + return ( + <> + + + ); +}; + +export default DexGuide; diff --git a/src/components/dex/TradingHeader/index.tsx b/src/components/dex/TradingHeader/index.tsx index 554f744..b75f657 100644 --- a/src/components/dex/TradingHeader/index.tsx +++ b/src/components/dex/TradingHeader/index.tsx @@ -4,13 +4,15 @@ import { ReactComponent as DownIcon } from '@/assets/images/UI/down_icon.svg'; import { ReactComponent as VolumeIcon } from '@/assets/images/UI/volume_icon.svg'; import BackButton from '@/components/default/BackButton/BackButton'; import { roundTo, notationToString, classes } from '@/utils/utils'; -// import questionIcon from '@/assets/images/UI/question.svg'; -// import Image from 'next/image'; +import questionIcon from '@/assets/images/UI/question.svg'; +import Image from 'next/image'; +import { useTour } from '@/store/guide-provider'; import styles from './styles.module.scss'; import StatItem from './components/StatItem'; import { TradingHeaderProps } from './types'; import CurrencyIcon from './components/CurrencyIcon'; import AssetRow from './components/AssetRow'; +import DexGuide from './components/DexGuide'; const TradingHeader = ({ pairStats, @@ -21,6 +23,8 @@ const TradingHeader = ({ secondAssetId, pairData, }: TradingHeaderProps) => { + const { start, isRunning } = useTour('dex-onboarding'); + const currencyNames = { firstCurrencyName: pairData?.first_currency?.name || '', secondCurrencyName: pairData?.second_currency?.name || '', @@ -59,76 +63,85 @@ const TradingHeader = ({ ]; return ( -
-
-
-
- -
+ <> + +
+
+
+
+ +
-
-

- {!pairData ? ( - '...' - ) : ( - <> - {firstCurrencyName} - /{secondCurrencyName} - - )} -

- -
-

= 0 ? styles.green : styles.red, +

+

+ {!pairData ? ( + '...' + ) : ( + <> + {firstCurrencyName} + /{secondCurrencyName} + )} - > - {roundTo(notationToString(pairStats?.rate || 0, 8))}

- {pairRateUsd &&

~ ${pairRateUsd}

} + +
+

= 0 ? styles.green : styles.red, + )} + > + {roundTo(notationToString(pairStats?.rate || 0, 8))} +

+ {pairRateUsd && ( +

~ ${pairRateUsd}

+ )} +
+ + {pairData && firstAssetLink && secondAssetLink && ( +
+ + +
+ )} + + {stats.map(({ Img, title, value, coefficient }) => ( + + ))}
- {pairData && firstAssetLink && secondAssetLink && ( -
- - -
- )} +
+ - {stats.map(({ Img, title, value, coefficient }) => ( - - ))} + +
- -
- {/* */} - - -
-
+ ); }; diff --git a/src/components/dex/UserOrders/index.tsx b/src/components/dex/UserOrders/index.tsx index 61e56df..89e1a9b 100644 --- a/src/components/dex/UserOrders/index.tsx +++ b/src/components/dex/UserOrders/index.tsx @@ -373,7 +373,7 @@ const UserOrders = ({ return ( <> -
+
+
@@ -238,7 +239,7 @@ function Trading() { )}