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() {
)}
- >
+
);
}
diff --git a/src/pages/dex/trading/find-pair/index.tsx b/src/pages/dex/trading/find-pair/index.tsx
new file mode 100644
index 0000000..24f94cf
--- /dev/null
+++ b/src/pages/dex/trading/find-pair/index.tsx
@@ -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 (
+
+
Error: {error}
+
+ );
+};
+export default Page;
diff --git a/src/store/guide-provider.tsx b/src/store/guide-provider.tsx
new file mode 100644
index 0000000..9e8021b
--- /dev/null
+++ b/src/store/guide-provider.tsx
@@ -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;
+
+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(null);
+const STORAGE_PREFIX = 'tour.seen:';
+
+export function GuideProvider({ children }: { children: React.ReactNode }) {
+ const registryRef = useRef({});
+ const [run, setRun] = useState(false);
+ const [active, setActive] = useState(undefined);
+ const [stepIndex, setStepIndex] = useState(0);
+
+ const register = useCallback((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((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(
+ () => ({
+ register,
+ start,
+ startOnce,
+ stop,
+ isRunning: run,
+ active,
+ }),
+ [register, start, startOnce, stop, run, active],
+ );
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+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;
diff --git a/src/styles/FindPair.module.scss b/src/styles/FindPair.module.scss
new file mode 100644
index 0000000..13e1f82
--- /dev/null
+++ b/src/styles/FindPair.module.scss
@@ -0,0 +1,6 @@
+.title {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
diff --git a/src/styles/themes/dark.scss b/src/styles/themes/dark.scss
index de9a109..0001323 100644
--- a/src/styles/themes/dark.scss
+++ b/src/styles/themes/dark.scss
@@ -56,4 +56,5 @@
--table-group-header-bg: #0c1940;
--tab-bg-color: #1f8feb1a;
--selector-bg-color: #0c1940;
+ --guide-skip-btn: #1f8feb;
}
diff --git a/src/styles/themes/light.scss b/src/styles/themes/light.scss
index 8a639d6..c4fdac0 100644
--- a/src/styles/themes/light.scss
+++ b/src/styles/themes/light.scss
@@ -56,4 +56,5 @@
--table-group-header-bg: #f2f5f9;
--tab-bg-color: #1f8feb;
--selector-bg-color: #e7eff8;
+ --guide-skip-btn: #ffffff;
}
diff --git a/src/utils/methods.ts b/src/utils/methods.ts
index 89d6a41..5e86cfb 100644
--- a/src/utils/methods.ts
+++ b/src/utils/methods.ts
@@ -81,7 +81,7 @@ export async function findPairID(
second: string,
host: string | undefined = undefined,
): Promise {
- const findPairURL = `${host ? `https://${host}` : ''}/api/dex/find-pair`;
+ const findPairURL = `${host ?? ''}/api/dex/find-pair`;
console.log('Find pair URL:', findPairURL);