Merge pull request #4 from jejolare-dev/staging

merge staging
This commit is contained in:
Dmitrii Kolpakov 2026-01-09 17:25:49 +07:00 committed by GitHub
commit 165544fe58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 727 additions and 76 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

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

View file

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

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

@ -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>
</>
);
};

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

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

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

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

@ -0,0 +1,6 @@
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

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

View file

@ -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);