commit
5b11600361
19 changed files with 727 additions and 76 deletions
140
package-lock.json
generated
140
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
12
src/components/UI/GuideContent/index.tsx
Normal file
12
src/components/UI/GuideContent/index.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useEffect } from 'react';
|
||||
import { GuideContentProps } from './types';
|
||||
|
||||
function GuideContent({ onEnter, text }: GuideContentProps) {
|
||||
useEffect(() => {
|
||||
if (onEnter) onEnter();
|
||||
}, [onEnter]);
|
||||
|
||||
return <p>{text}</p>;
|
||||
}
|
||||
|
||||
export default GuideContent;
|
||||
4
src/components/UI/GuideContent/types.ts
Normal file
4
src/components/UI/GuideContent/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface GuideContentProps {
|
||||
text: string;
|
||||
onEnter?: () => void;
|
||||
}
|
||||
37
src/components/UI/GuideTooltip/index.tsx
Normal file
37
src/components/UI/GuideTooltip/index.tsx
Normal file
|
|
@ -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 (
|
||||
<div {...tooltipProps} className={styles.tooltip}>
|
||||
<div className={styles.tooltip__body}>
|
||||
<div className={styles.tooltip__index}>
|
||||
{index + 1}/{size}
|
||||
</div>
|
||||
<div className={styles.tooltip__content}>
|
||||
{typeof step.content === 'string' ? <p>{step.content}</p> : step.content}
|
||||
</div>
|
||||
|
||||
<Button {...(primaryProps as object)} className={styles.tooltip__btn}>
|
||||
{isLastStep ? 'Close' : 'Continue'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!isLastStep && (
|
||||
<button {...skipProps} className={styles.tooltip__skip}>
|
||||
Skip guide
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
src/components/UI/GuideTooltip/styles.module.scss
Normal file
77
src/components/UI/GuideTooltip/styles.module.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/components/default/GuideRegistrator/index.tsx
Normal file
16
src/components/default/GuideRegistrator/index.tsx
Normal file
|
|
@ -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 <></>;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={styles.inputPanel}>
|
||||
<div data-tour="input-panel" className={styles.inputPanel}>
|
||||
{hasImmediateMatch && (
|
||||
<Alert
|
||||
type="custom"
|
||||
|
|
@ -165,7 +171,7 @@ function InputPanelItem(props: InputPanelItemProps) {
|
|||
className={styles.applyAlert__button}
|
||||
onClick={() => {
|
||||
scrollToOrderList();
|
||||
goToSuitableTab();
|
||||
goToTab('matches');
|
||||
setHasImmediateMatch(false);
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ const OrdersPool = (props: OrdersPoolProps) => {
|
|||
useMouseLeave(ordersInfoRef, () => setOrdersInfoTooltip(null));
|
||||
return (
|
||||
<>
|
||||
<div className={styles.ordersPool}>
|
||||
<div data-tour="orders-pool" className={styles.ordersPool}>
|
||||
<div className={styles.ordersPool__header}>
|
||||
<Tabs value={currentOrder} setValue={setCurrentOrder} data={tabsData} />
|
||||
|
||||
|
|
|
|||
102
src/components/dex/TradingHeader/components/DexGuide/index.tsx
Normal file
102
src/components/dex/TradingHeader/components/DexGuide/index.tsx
Normal file
|
|
@ -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: (
|
||||
<GuideContent text="This section displays the current buy and sell orders in the market. You can see the price, quantity, and total value, as well as the order book depth visualized with bars." />
|
||||
),
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tour="input-panel"]',
|
||||
placement: getPlacement('left-start'),
|
||||
content: (
|
||||
<GuideContent text="Here you can place buy or sell orders. Set your price, choose the amount, and review the total cost before creating the order." />
|
||||
),
|
||||
},
|
||||
{
|
||||
target: '[data-tour="user-orders"]',
|
||||
placement: getPlacement('top-start'),
|
||||
content: (
|
||||
<GuideContent
|
||||
onEnter={() => 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: (
|
||||
<GuideContent
|
||||
onEnter={() => changeTab('matches')}
|
||||
text="Here you see orders that have been successfully matched and executed."
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
target: '[data-tour="user-orders"]',
|
||||
placement: getPlacement('top-start'),
|
||||
content: (
|
||||
<GuideContent
|
||||
onEnter={() => 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: (
|
||||
<GuideContent
|
||||
onEnter={() => changeTab('offers')}
|
||||
text="In this tab you can find offers you’ve made to other traders that are not yet matched."
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<GuideRegistrator name="dex-onboarding" steps={steps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DexGuide;
|
||||
|
|
@ -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 (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.header__stats}>
|
||||
<div className={styles.header__currency}>
|
||||
<div className={styles.header__currency_icon}>
|
||||
<CurrencyIcon code={firstAssetId} />
|
||||
</div>
|
||||
<>
|
||||
<DexGuide />
|
||||
<div className={styles.header}>
|
||||
<div className={styles.header__stats}>
|
||||
<div className={styles.header__currency}>
|
||||
<div className={styles.header__currency_icon}>
|
||||
<CurrencyIcon code={firstAssetId} />
|
||||
</div>
|
||||
|
||||
<div className={styles.header__currency_item}>
|
||||
<p className={styles.currencyName}>
|
||||
{!pairData ? (
|
||||
'...'
|
||||
) : (
|
||||
<>
|
||||
{firstCurrencyName}
|
||||
<span>/{secondCurrencyName}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className={styles.price}>
|
||||
<p
|
||||
className={classes(
|
||||
styles.price__secondCurrency,
|
||||
coefficientOutput >= 0 ? styles.green : styles.red,
|
||||
<div className={styles.header__currency_item}>
|
||||
<p className={styles.currencyName}>
|
||||
{!pairData ? (
|
||||
'...'
|
||||
) : (
|
||||
<>
|
||||
{firstCurrencyName}
|
||||
<span>/{secondCurrencyName}</span>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{roundTo(notationToString(pairStats?.rate || 0, 8))}
|
||||
</p>
|
||||
{pairRateUsd && <p className={styles.price__usd}>~ ${pairRateUsd}</p>}
|
||||
|
||||
<div className={styles.price}>
|
||||
<p
|
||||
className={classes(
|
||||
styles.price__secondCurrency,
|
||||
coefficientOutput >= 0 ? styles.green : styles.red,
|
||||
)}
|
||||
>
|
||||
{roundTo(notationToString(pairStats?.rate || 0, 8))}
|
||||
</p>
|
||||
{pairRateUsd && (
|
||||
<p className={styles.price__usd}>~ ${pairRateUsd}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pairData && firstAssetLink && secondAssetLink && (
|
||||
<div className={styles.header__stats_assets}>
|
||||
<AssetRow
|
||||
name={firstCurrencyName}
|
||||
link={firstAssetLink}
|
||||
id={firstAssetId || ''}
|
||||
code={firstAssetId}
|
||||
/>
|
||||
<AssetRow
|
||||
name={secondCurrencyName}
|
||||
link={secondAssetLink}
|
||||
id={secondAssetId || ''}
|
||||
code={secondAssetId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.map(({ Img, title, value, coefficient }) => (
|
||||
<StatItem
|
||||
key={title}
|
||||
className={styles.header__stats_item}
|
||||
Img={Img}
|
||||
title={title}
|
||||
value={value}
|
||||
coefficient={coefficient}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pairData && firstAssetLink && secondAssetLink && (
|
||||
<div className={styles.header__stats_assets}>
|
||||
<AssetRow
|
||||
name={firstCurrencyName}
|
||||
link={firstAssetLink}
|
||||
id={firstAssetId || ''}
|
||||
code={firstAssetId}
|
||||
/>
|
||||
<AssetRow
|
||||
name={secondCurrencyName}
|
||||
link={secondAssetLink}
|
||||
id={secondAssetId || ''}
|
||||
code={secondAssetId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.header__actions}>
|
||||
<button
|
||||
onClick={start}
|
||||
disabled={isRunning}
|
||||
className={styles.header__actions_guide}
|
||||
>
|
||||
<Image src={questionIcon} width={24} height={24} alt="guide" />
|
||||
</button>
|
||||
|
||||
{stats.map(({ Img, title, value, coefficient }) => (
|
||||
<StatItem
|
||||
key={title}
|
||||
className={styles.header__stats_item}
|
||||
Img={Img}
|
||||
title={title}
|
||||
value={value}
|
||||
coefficient={coefficient}
|
||||
/>
|
||||
))}
|
||||
<BackButton className={styles.header__actions_backBtn} isSm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.header__actions}>
|
||||
{/* <button className={styles.header__actions_guide}>
|
||||
<Image src={questionIcon} width={24} height={24} alt="guide" />
|
||||
</button> */}
|
||||
|
||||
<BackButton className={styles.header__actions_backBtn} isSm />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ const UserOrders = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div ref={orderListRef} className={styles.userOrders}>
|
||||
<div data-tour="user-orders" ref={orderListRef} className={styles.userOrders}>
|
||||
<div className={styles.userOrders__header}>
|
||||
<Tabs
|
||||
type={isMobile ? 'button' : 'tab'}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import useTradeInit from '@/hook/useTradeInit';
|
|||
import useMatrixAddresses from '@/hook/useMatrixAddresses';
|
||||
import takeOrderClick from '@/utils/takeOrderClick';
|
||||
import useUpdateUser from '@/hook/useUpdateUser';
|
||||
import { GuideProvider } from '@/store/guide-provider';
|
||||
|
||||
function Trading() {
|
||||
const { alertState, alertSubtitle, setAlertState } = useAlert();
|
||||
|
|
@ -141,7 +142,7 @@ function Trading() {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<GuideProvider>
|
||||
<Header isLg={true} />
|
||||
|
||||
<main className={styles.trading}>
|
||||
|
|
@ -238,7 +239,7 @@ function Trading() {
|
|||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
</GuideProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
51
src/pages/dex/trading/find-pair/index.tsx
Normal file
51
src/pages/dex/trading/find-pair/index.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { GetServerSideProps } from 'next';
|
||||
|
||||
import { findPairID } from '@/utils/methods';
|
||||
import styles from '@/styles/404.module.scss';
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const { first, second } = context.query;
|
||||
|
||||
if (!first || !second) {
|
||||
return {
|
||||
notFound: true, // Show a 404 page if parameters are missing
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const idFound = await findPairID(first as string, second as string, API_URL);
|
||||
|
||||
console.log('ID found:', idFound);
|
||||
|
||||
if (typeof idFound === 'number') {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/dex/trading/${idFound}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching pair ID:', error);
|
||||
return {
|
||||
props: {
|
||||
error: 'Failed to resolve the pair.',
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Page = ({ error }: { error?: string }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1 className={styles.title}>Error: {error}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
191
src/store/guide-provider.tsx
Normal file
191
src/store/guide-provider.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import GuideTooltip from '@/components/UI/GuideTooltip';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
import { CallBackProps, EVENTS, STATUS, Step } from 'react-joyride';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const JoyrideNoSSR = dynamic(() => import('react-joyride'), { ssr: false });
|
||||
|
||||
type TourName = string;
|
||||
type Registry = Record<TourName, Step[]>;
|
||||
|
||||
type Ctx = {
|
||||
register: (_name: TourName, _steps: Step[]) => void;
|
||||
start: (_name: TourName) => void;
|
||||
startOnce: (_name: TourName, _version?: string) => void;
|
||||
stop: () => void;
|
||||
isRunning: boolean;
|
||||
active?: TourName;
|
||||
};
|
||||
|
||||
const TourCtx = createContext<Ctx | null>(null);
|
||||
const STORAGE_PREFIX = 'tour.seen:';
|
||||
|
||||
export function GuideProvider({ children }: { children: React.ReactNode }) {
|
||||
const registryRef = useRef<Registry>({});
|
||||
const [run, setRun] = useState(false);
|
||||
const [active, setActive] = useState<TourName | undefined>(undefined);
|
||||
const [stepIndex, setStepIndex] = useState(0);
|
||||
|
||||
const register = useCallback<Ctx['register']>((name, steps) => {
|
||||
registryRef.current[name] = steps;
|
||||
}, []);
|
||||
|
||||
const start = useCallback((name: TourName) => {
|
||||
if (!registryRef.current[name]?.length) return;
|
||||
setActive(name);
|
||||
setStepIndex(0);
|
||||
setRun(true);
|
||||
}, []);
|
||||
|
||||
const startOnce = useCallback(
|
||||
(name: TourName, version = 'v1') => {
|
||||
const key = `${STORAGE_PREFIX}${name}:${version}`;
|
||||
if (!localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, '1');
|
||||
start(name);
|
||||
}
|
||||
},
|
||||
[start],
|
||||
);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setRun(false);
|
||||
setActive(undefined);
|
||||
setStepIndex(0);
|
||||
}, []);
|
||||
|
||||
const waitForEl = useCallback(
|
||||
(selector: string | Element, { timeout = 2000, interval = 50 } = {}) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
const startT = performance.now();
|
||||
const tick = () => {
|
||||
let found: Element | null | string = selector;
|
||||
|
||||
if (selector === 'body') {
|
||||
found = document.body;
|
||||
} else if (typeof selector === 'string') {
|
||||
found = document.querySelector(selector);
|
||||
} else {
|
||||
found = selector;
|
||||
}
|
||||
|
||||
if (found) return resolve(true);
|
||||
if (performance.now() - startT > timeout) return resolve(false);
|
||||
setTimeout(tick, interval);
|
||||
};
|
||||
tick();
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const advancingRef = useRef(false);
|
||||
|
||||
const safeAdvanceTo = useCallback(
|
||||
async (nextIndex: number) => {
|
||||
if (advancingRef.current) return;
|
||||
advancingRef.current = true;
|
||||
|
||||
const steps = active ? (registryRef.current[active] ?? []) : [];
|
||||
const next = steps[nextIndex];
|
||||
|
||||
if (!next || next.target === 'body') {
|
||||
setStepIndex(nextIndex);
|
||||
advancingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await waitForEl(String(next.target), { timeout: 2000 });
|
||||
if (ok) setStepIndex(nextIndex);
|
||||
|
||||
advancingRef.current = false;
|
||||
},
|
||||
[active, waitForEl],
|
||||
);
|
||||
|
||||
const callback = useCallback(
|
||||
(data: CallBackProps) => {
|
||||
const { status, index, type } = data;
|
||||
|
||||
if (type === EVENTS.STEP_AFTER && typeof index === 'number') {
|
||||
void safeAdvanceTo(index + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === EVENTS.TARGET_NOT_FOUND && typeof index === 'number') {
|
||||
void safeAdvanceTo(index + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
||||
stop();
|
||||
setStepIndex(0);
|
||||
}
|
||||
},
|
||||
[safeAdvanceTo, stop],
|
||||
);
|
||||
|
||||
const steps = active ? (registryRef.current[active] ?? []) : [];
|
||||
|
||||
const value = useMemo<Ctx>(
|
||||
() => ({
|
||||
register,
|
||||
start,
|
||||
startOnce,
|
||||
stop,
|
||||
isRunning: run,
|
||||
active,
|
||||
}),
|
||||
[register, start, startOnce, stop, run, active],
|
||||
);
|
||||
|
||||
return (
|
||||
<TourCtx.Provider value={value}>
|
||||
{children}
|
||||
<JoyrideNoSSR
|
||||
steps={steps}
|
||||
run={run}
|
||||
stepIndex={stepIndex}
|
||||
continuous
|
||||
showProgress
|
||||
showSkipButton
|
||||
hideCloseButton
|
||||
scrollToFirstStep
|
||||
disableScrolling={false}
|
||||
spotlightPadding={0}
|
||||
disableOverlayClose
|
||||
disableCloseOnEsc
|
||||
floaterProps={{
|
||||
disableAnimation: true,
|
||||
styles: {
|
||||
floater: { transition: 'none', position: 'fixed' },
|
||||
},
|
||||
hideArrow: true,
|
||||
}}
|
||||
tooltipComponent={GuideTooltip}
|
||||
styles={{
|
||||
options: { zIndex: 9999, primaryColor: '#1F8FEB' },
|
||||
overlay: { transition: 'none' },
|
||||
tooltipContainer: { transition: 'none' },
|
||||
}}
|
||||
locale={{ next: 'Continue', skip: 'Skip guide', last: 'Finish' }}
|
||||
callback={callback}
|
||||
/>
|
||||
</TourCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTour(name?: TourName) {
|
||||
const ctx = useContext(TourCtx);
|
||||
if (!ctx) throw new Error('useTour must be used within GuideProvider');
|
||||
const { register, start, startOnce, stop, isRunning, active } = ctx;
|
||||
return {
|
||||
register,
|
||||
start: () => (name ? start(name) : stop()),
|
||||
startOnce: (version?: string) => (name ? startOnce(name, version) : undefined),
|
||||
stop,
|
||||
isRunning,
|
||||
active,
|
||||
};
|
||||
}
|
||||
|
||||
export const dataTour = (id: string) => ({ 'data-tour': id }) as const;
|
||||
6
src/styles/FindPair.module.scss
Normal file
6
src/styles/FindPair.module.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.title {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
|
@ -56,4 +56,5 @@
|
|||
--table-group-header-bg: #0c1940;
|
||||
--tab-bg-color: #1f8feb1a;
|
||||
--selector-bg-color: #0c1940;
|
||||
--guide-skip-btn: #1f8feb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,4 +56,5 @@
|
|||
--table-group-header-bg: #f2f5f9;
|
||||
--tab-bg-color: #1f8feb;
|
||||
--selector-bg-color: #e7eff8;
|
||||
--guide-skip-btn: #ffffff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export async function findPairID(
|
|||
second: string,
|
||||
host: string | undefined = undefined,
|
||||
): Promise<number | undefined> {
|
||||
const findPairURL = `${host ? `https://${host}` : ''}/api/dex/find-pair`;
|
||||
const findPairURL = `${host ?? ''}/api/dex/find-pair`;
|
||||
|
||||
console.log('Find pair URL:', findPairURL);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue