Merge pull request #19 from jejolare-dev/dev

add: interactive guide
This commit is contained in:
Dmitrii Kolpakov 2025-09-06 19:36:35 +07:00 committed by GitHub
commit ee5ad268c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 841 additions and 249 deletions

140
package-lock.json generated
View file

@ -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",

View file

@ -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",

View 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;

View file

@ -0,0 +1,4 @@
export interface GuideContentProps {
text: string;
onEnter?: () => void;
}

View 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>
);
}

View 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;
}
}
}

View 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 <></>;
}

View file

@ -48,7 +48,7 @@
align-items: center;
justify-content: center;
>p {
> p {
font-size: 14px;
font-weight: 400;
white-space: nowrap;
@ -76,9 +76,9 @@
&__currency {
padding: 0 8px;
>p {
> p {
font-size: 12px;
}
}
}
}
}

View file

@ -50,9 +50,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,
@ -93,6 +97,8 @@ function InputPanelItem(props: InputPanelItemProps) {
if (result.success) {
if (result.data?.immediateMatch) {
setHasImmediateMatch(true);
goToTab();
scrollToOrderList();
}
onAfter();
resetForm();
@ -125,7 +131,7 @@ function InputPanelItem(props: InputPanelItemProps) {
const isButtonDisabled = !priceValid || !amountValid || !totalValid || creatingState;
return (
<div className={styles.inputPanel}>
<div data-tour="input-panel" className={styles.inputPanel}>
{hasImmediateMatch && (
<Alert
type="custom"
@ -139,7 +145,7 @@ function InputPanelItem(props: InputPanelItemProps) {
className={styles.applyAlert__button}
onClick={() => {
scrollToOrderList();
goToSuitableTab();
goToTab('matches');
setHasImmediateMatch(false);
}}
>

View file

@ -144,7 +144,6 @@
}
}
@media screen and (max-width: 640px) {
.inputPanel {
padding: 0;
@ -195,4 +194,4 @@
}
}
}
}
}

View file

@ -170,7 +170,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} />

View file

@ -262,4 +262,4 @@
.tooltip {
display: none;
}
}
}

View 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 youve made to other traders that are not yet matched."
/>
),
},
];
return (
<>
<GuideRegistrator name="dex-onboarding" steps={steps} />
</>
);
};
export default DexGuide;

View file

@ -6,11 +6,13 @@ 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 { 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>
</>
);
};

View file

@ -22,7 +22,7 @@
background-color: var(--icon-bg-color);
border-radius: 50%;
>img {
> img {
width: 24px;
height: auto;
}
@ -169,4 +169,4 @@
gap: 12px;
}
}
}
}

View file

@ -1,206 +1,206 @@
.cards {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 15px;
padding: 6px 10px;
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 15px;
padding: 6px 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 2px;
height: 100%;
}
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 2px;
height: 100%;
}
&.sell {
&::before {
background-color: #ff6767;
}
&.sell {
&::before {
background-color: #ff6767;
}
.card__type {
color: #ff6767;
text-transform: capitalize;
}
}
.card__type {
color: #ff6767;
text-transform: capitalize;
}
}
&.buy {
&::before {
background-color: #16d1d6;
}
&.buy {
&::before {
background-color: #16d1d6;
}
.card__type {
color: #16d1d6;
text-transform: capitalize;
}
}
.card__type {
color: #16d1d6;
text-transform: capitalize;
}
}
&__row {
width: 100%;
display: flex;
align-items: center;
gap: 40px;
&__row {
width: 100%;
display: flex;
align-items: center;
gap: 40px;
&.sm {
gap: 18px;
&.sm {
gap: 18px;
.card__col {
min-width: 50px;
}
}
}
.card__col {
min-width: 50px;
}
}
}
&__label {
color: var(--table-th-color);
font-size: 12px;
font-weight: 500;
line-height: 100%;
}
&__label {
color: var(--table-th-color);
font-size: 12px;
font-weight: 500;
line-height: 100%;
}
&__value {
font-size: 12px;
font-weight: 500;
line-height: 100%;
&__value {
font-size: 12px;
font-weight: 500;
line-height: 100%;
&.primary {
color: #1f8feb;
}
&.primary {
color: #1f8feb;
}
&.secondary {
color: var(--table-th-color);
}
&.secondary {
color: var(--table-th-color);
}
span {
color: var(--table-th-color);
font-size: 11px;
font-weight: 500;
line-height: 100%;
}
}
span {
color: var(--table-th-color);
font-size: 11px;
font-weight: 500;
line-height: 100%;
}
}
&__col {
min-width: 100px;
display: flex;
flex-direction: column;
gap: 10px;
}
&__col {
min-width: 100px;
display: flex;
flex-direction: column;
gap: 10px;
}
&__pair {
.card__value {
display: flex;
align-items: center;
gap: 8px;
}
&__pair {
.card__value {
display: flex;
align-items: center;
gap: 8px;
}
.card__type {
display: none;
}
}
.card__type {
display: none;
}
}
&__flex {
gap: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
&__flex {
gap: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
&__actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
}
&__actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
}
.mobile {
display: none;
}
.mobile {
display: none;
}
}
@media screen and (max-width: 640px) {
.cards .card {
gap: 10px;
.cards .card {
gap: 10px;
&__row {
gap: 15px;
&__row {
gap: 15px;
&.sm {
gap: 5px;
}
}
&.sm {
gap: 5px;
}
}
&__col {
min-width: 80px;
&__col {
min-width: 80px;
&_direction {
display: none;
}
}
&_direction {
display: none;
}
}
&__label {
font-size: 10px;
}
&__label {
font-size: 10px;
}
&__value {
font-size: 11px;
}
&__value {
font-size: 11px;
}
&__pair {
.card__label {
display: none;
}
&__pair {
.card__label {
display: none;
}
.card__value {
font-size: 12px;
}
.card__value {
font-size: 12px;
}
.card__type {
font-size: 12px;
display: inline-block;
}
}
}
.card__type {
font-size: 12px;
display: inline-block;
}
}
}
}
@media screen and (max-width: 500px) {
.cards .card {
&__row {
justify-content: space-between;
.cards .card {
&__row {
justify-content: space-between;
&.sm {
justify-content: flex-start;
}
}
&.sm {
justify-content: flex-start;
}
}
&__col {
min-width: auto;
}
&__col {
min-width: auto;
}
&__flex {
flex-direction: column;
}
&__flex {
flex-direction: column;
}
&__price {
display: none;
&__price {
display: none;
&.mobile {
display: flex;
}
}
&.mobile {
display: flex;
}
}
&__offers {
display: none;
&__offers {
display: none;
&.mobile {
display: flex;
}
}
}
}
&.mobile {
display: flex;
}
}
}
}

View file

@ -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'}

View file

@ -57,7 +57,7 @@
td {
position: relative;
>p {
> p {
width: 100%;
font-size: 12px;
font-weight: 400;
@ -76,7 +76,7 @@
}
}
>span {
> span {
line-height: 1;
color: var(--font-dimmed-color);
font-size: 11px;
@ -115,4 +115,4 @@
margin-bottom: 10px;
}
}
}
}

View file

@ -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();
@ -137,7 +138,7 @@ function Trading() {
};
return (
<>
<GuideProvider>
<Header isLg={true} />
<main className={styles.trading}>
@ -233,7 +234,7 @@ function Trading() {
)}
</main>
<Footer />
</>
</GuideProvider>
);
}

View 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;

View file

@ -68,7 +68,6 @@
@media screen and (max-width: 1024px) {
.trading__top {
&_trades,
&_form {
max-width: 100%;
@ -90,4 +89,4 @@
.trading__top {
height: 370px;
}
}
}

View file

@ -56,4 +56,5 @@
--table-group-header-bg: #0c1940;
--tab-bg-color: #1f8feb1a;
--selector-bg-color: #0c1940;
--guide-skip-btn: #1f8feb;
}

View file

@ -56,4 +56,5 @@
--table-group-header-bg: #f2f5f9;
--tab-bg-color: #1f8feb;
--selector-bg-color: #e7eff8;
--guide-skip-btn: #ffffff;
}