refactoring: dex/trading

This commit is contained in:
AzizbekFayziyev 2025-08-08 16:40:10 +05:00
parent e9b6c353b2
commit dbdfbd1ed6
80 changed files with 3453 additions and 3177 deletions

View file

@ -1,17 +1,17 @@
.content__preloader__wrapper {
.loader {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
> div {
&__content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
p {
&__text {
color: var(--font-dimmed-color);
}
}

View file

@ -1,12 +1,15 @@
import Preloader from '@/components/UI/Preloader/Preloader';
import { classes } from '@/utils/utils';
import styles from './ContentPreloader.module.scss';
import { ContentPreloaderProps } from './types';
function ContentPreloader(props: { className?: string }) {
function ContentPreloader({ className, style }: ContentPreloaderProps) {
return (
<div className={`${styles.content__preloader__wrapper} ${props.className}`}>
<div>
<div style={style} className={classes(styles.loader, className)}>
<div className={styles.loader__content}>
<Preloader />
<p>Loading...</p>
<p className={styles.loder__text}>Loading...</p>
</div>
</div>
);

View file

@ -0,0 +1,6 @@
import { CSSProperties } from 'react';
export interface ContentPreloaderProps {
className?: string;
style?: CSSProperties;
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import { ReactComponent as NoOffersIcon } from '@/assets/images/UI/no_offers.svg';
import { classes } from '@/utils/utils';
import styles from './styles.module.scss';
import { EmptyMessageProps } from './types';
const EmptyMessage = ({ text, customIcon }: EmptyMessageProps) => {
return (
<div className={classes(styles.empty, styles.all__orders__msg)}>
{!customIcon ? <NoOffersIcon className={styles.empty__icon} /> : customIcon}
<h6 className={styles.empty__text}>{text}</h6>
</div>
);
};
export default EmptyMessage;

View file

@ -0,0 +1,17 @@
.empty {
width: 100%;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
&__text {
color: var(--font-dimmed-color);
}
&__icon {
transform: scale(0.8);
}
}

View file

@ -0,0 +1,6 @@
import { ReactNode } from 'react';
export interface EmptyMessageProps {
text: string;
customIcon?: ReactNode;
}

View file

@ -9,7 +9,7 @@
background: none;
outline: none;
border: none;
z-index: 5;
z-index: 2;
cursor: pointer;
}
@ -81,18 +81,4 @@
border: 2px solid var(--window-bg-color);
transition: background 0.3s ease-in-out;
}
// .input__range::-webkit-slider-runnable-track::-webkit-slider-thumb {
// background-color: #fff;
// border: 2px solid #555;
// }
// .input__range::-webkit-slider-runnable-track {
// -webkit-appearance: none;
// height: 20px;
// background-color: #1F8FEB;
// border: 1px solid #ffffff;
// box-shadow: none;
// background: transparent;
// }
}

View file

@ -11,11 +11,14 @@
position: relative;
&.lg {
padding: 0 60px;
padding-inline: 60px;
@media screen and (max-width: 1060px) {
padding-right: 20px;
padding-left: 20px;
@media screen and (max-width: 1600px) {
padding-inline: 40px;
}
@media screen and (max-width: 1200px) {
padding-inline: 20px;
}
}

36
src/constants/index.ts Normal file
View file

@ -0,0 +1,36 @@
import PeriodState from '@/interfaces/states/pages/dex/trading/InputPanelItem/PeriodState';
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
export const periods: PeriodState[] = [
{
name: '1H',
code: '1h',
},
{
name: '1D',
code: '1d',
},
{
name: '1W',
code: '1w',
},
{
name: '1M',
code: '1m',
},
];
export const buySellValues: SelectValue[] = [
{
name: 'All',
code: 'all',
},
{
name: 'Buy',
code: 'buy',
},
{
name: 'Sell',
code: 'sell',
},
];

22
src/hook/useAlert.ts Normal file
View file

@ -0,0 +1,22 @@
import AlertType from '@/interfaces/common/AlertType';
import { Store } from '@/store/store-reducer';
import { useContext } from 'react';
export const useAlert = () => {
const { state, dispatch } = useContext(Store);
const setAlertState = (state: AlertType) => {
dispatch({ type: 'ALERT_STATE_UPDATED', payload: state });
};
const setAlertSubtitle = (subtitle: string) => {
dispatch({ type: 'ALERT_SUBTITLE_UPDATED', payload: subtitle });
};
return {
alertState: state.alertState,
alertSubtitle: state.alertSubtitle,
setAlertState,
setAlertSubtitle,
};
};

21
src/hook/useMouseLeave.ts Normal file
View file

@ -0,0 +1,21 @@
import { ForwardedRef, useEffect } from 'react';
const useMouseLeave = (ref: ForwardedRef<HTMLElement>, callbackFn: () => void) => {
useEffect(() => {
const targetEl = (event: MouseEvent) => {
if (ref && typeof ref !== 'function' && ref.current) {
if (ref?.current && !ref?.current.contains(event.target as Node)) {
callbackFn();
}
}
};
window.addEventListener('mousemove', targetEl);
return () => {
window.removeEventListener('mousemove', targetEl);
};
}, []);
};
export default useMouseLeave;

20
src/hook/useScroll.ts Normal file
View file

@ -0,0 +1,20 @@
import { useCallback, useRef } from 'react';
function useScroll<T extends HTMLElement>() {
const elementRef = useRef<T | null>(null);
const scrollToElement = useCallback((options?: ScrollIntoViewOptions) => {
if (elementRef.current) {
elementRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
...options,
});
}
}, []);
return { elementRef, scrollToElement };
}
export default useScroll;

View file

@ -1,6 +1,7 @@
import { Dispatch } from 'react';
import { GetUserResData } from '../responses/user/GetUserRes';
import { GetConfigResData } from '../responses/config/GetConfigRes';
import AlertType from './AlertType';
export interface Asset {
name: string;
@ -50,6 +51,8 @@ interface ContextState {
offers: number;
};
closed_notifications: number[];
alertState: AlertType;
alertSubtitle: string;
}
type ContextAction =
@ -75,6 +78,14 @@ type ContextAction =
| {
type: 'CLOSED_NOTIFICATIONS_UPDATED';
payload: number[];
}
| {
type: 'ALERT_STATE_UPDATED';
payload: AlertType;
}
| {
type: 'ALERT_SUBTITLE_UPDATED';
payload: string;
};
interface ContextValue {

View file

@ -0,0 +1,6 @@
interface MatrixAddress {
address: string;
registered: boolean;
}
export default MatrixAddress;

View file

@ -0,0 +1,24 @@
import { Dispatch, SetStateAction } from 'react';
interface OrderFormOutput {
price: string;
amount: string;
total: string;
priceValid: boolean;
amountValid: boolean;
totalValid: boolean;
totalUsd: string | undefined;
rangeInputValue: string;
setRangeInputValue: Dispatch<SetStateAction<string>>;
onPriceChange: (_inputValue: string) => void;
onAmountChange: (_inputValue: string) => void;
resetForm: () => void;
setTotal: Dispatch<SetStateAction<string>>;
setPrice: Dispatch<SetStateAction<string>>;
setAmount: Dispatch<SetStateAction<string>>;
setPriceValid: Dispatch<SetStateAction<boolean>>;
setAmountValid: Dispatch<SetStateAction<boolean>>;
setTotalValid: Dispatch<SetStateAction<boolean>>;
}
export default OrderFormOutput;

View file

@ -1,22 +1,20 @@
import AlertType from '@/interfaces/common/AlertType';
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
import { Dispatch, SetStateAction } from 'react';
interface InputPanelItemProps {
currencyNames: {
firstCurrencyName: string;
secondCurrencyName: string;
};
priceState: string;
amountState: string;
totalState: string;
buySellValues: SelectValue[];
buySellState: SelectValue;
// setBuySellState: Dispatch<SetStateAction<SelectValue>>;
setPriceFunction: (_value: string) => void;
setAmountFunction: (_value: string) => void;
setAlertState: Dispatch<SetStateAction<AlertType>>;
setAlertSubtitle: Dispatch<SetStateAction<string>>;
setRangeInputValue: Dispatch<SetStateAction<string>>;
rangeInputValue: string;
firstCurrencyName: string;
secondCurrencyName: string;
balance: number | undefined;
amountValid: boolean;
priceValid: boolean;

View file

@ -5,6 +5,7 @@ interface StatItemProps {
title: string;
value: string;
coefficient?: number;
className?: string;
}
export default StatItemProps;

View file

@ -1,5 +1,6 @@
import '@/styles/globals.scss';
import '@/styles/themes/light.scss';
import '@/styles/themes/dark.scss';
import Head from 'next/head';
import { StoreProvider } from '@/store/store-reducer';
import NextApp, { AppContext, AppProps } from 'next/app';

View file

@ -1,243 +0,0 @@
.input_panel__item {
width: 100%;
&_header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
h5 {
font-size: 16px;
font-weight: 600;
}
.input_panel__fees {
display: flex;
justify-content: space-between;
font-weight: 400;
font-size: 12px;
p {
color: var(--table-th-color);
font-weight: 400;
font-size: 12px;
}
span {
font-weight: 400;
font-size: 12px;
}
}
}
&_body {
display: flex;
flex-direction: column;
gap: 10px;
button {
margin-top: 10px;
}
> .buy_btn {
background-color: #16d1d6;
&:hover {
background-color: #45dade;
}
}
> .sell_btn {
background-color: #ff6767;
&:hover {
background-color: #ff8585;
}
}
.input_panel__range {
margin-top: 10px;
}
.input_panel__expiration {
display: flex;
justify-content: space-between;
gap: 20px;
h6 {
white-space: nowrap;
}
.expiration__dropdown {
width: 100%;
}
> div:first-child {
display: flex;
align-items: center;
gap: 6px;
}
@media screen and (max-width: 1500px) {
flex-wrap: wrap;
}
@media screen and (max-width: 1000px) {
flex-wrap: nowrap;
}
@media screen and (max-width: 530px) {
flex-wrap: wrap;
}
}
.labeled_input {
display: flex;
flex-direction: column;
gap: 8px;
h6 {
font-size: 11px;
font-family: 700;
color: var(--table-th-color);
}
> div {
width: 100%;
position: relative;
background-color: var(--bordered-input-bg);
border: 1px solid var(--window-border-color);
border-radius: 8px;
display: flex;
overflow: hidden;
input {
width: 100%;
padding: 13px 15px;
background-color: transparent;
border: none;
font-size: 16px;
font-weight: 400;
}
.labeled_input__value {
padding-right: 10px;
display: flex;
align-items: center;
> p {
color: var(--table-th-color);
font-size: 12px;
font-weight: 400;
}
}
.labeled_input__currency {
min-width: 82px;
max-width: 150px;
padding: 0 15px;
background-color: var(--dex-input-currency);
display: flex;
align-items: center;
justify-content: center;
> p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
&.labeled_input__invalid > div {
border-color: #ff6767;
}
@media screen and (max-width: 430px) {
> div {
input,
.labeled_input__value > p,
.labeled_input__currency > p {
font-size: 13px;
}
input {
padding: 19px 15px;
}
.labeled_input__currency {
min-width: 70px;
}
}
}
}
}
}
.buy-sell-switch {
padding: 3px;
height: 30px;
display: flex;
align-items: center;
border-radius: 100px;
border: 1px solid var(--dex-buy-sell-border);
.buy-sell-switch__item {
width: 50px;
height: 100%;
background-color: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 600;
border-radius: 100px;
&.item_selected-buy {
background-color: #16d1d6;
color: #ffffff;
}
&.item_selected-sell {
background-color: #ff6767;
color: #ffffff;
}
}
}
.apply__alert {
display: flex;
gap: 20px;
align-items: center;
&__content {
display: flex;
flex-direction: column;
gap: 10px;
}
&__button {
max-width: 125px;
background-color: var(--alert-btn-bg);
color: #1f8feb;
padding: 7px 32px;
font-size: 12px;
font-weight: 500;
&:hover {
background-color: var(--alert-btn-hover);
}
}
h2 {
font-size: 16px;
font-weight: 600;
}
p {
font-size: 14px;
opacity: 0.7;
margin-bottom: 5px;
}
}

View file

@ -1,31 +0,0 @@
.orders-buy-sell-switch {
display: flex;
align-items: center;
gap: 8px;
background-color: transparent !important;
cursor: pointer;
&:hover {
opacity: 0.7;
}
> p {
font-size: 16px;
font-weight: 600;
color: #16d1d6;
}
> svg > * {
fill: transparent;
}
&.orders-buy-sell-switch_sell {
> p {
color: #ff6767;
}
> svg > * {
stroke: #ff6767;
}
}
}

View file

@ -1,33 +0,0 @@
import HorizontalSelectProps from '@/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps';
import { ReactComponent as ArrowIcon } from '@/assets/images/UI/trade_arrow.svg';
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
import { classes } from '@/utils/utils';
import styles from './OrdersBuySellSwitch.module.scss';
export default function OrdersBuySellSwitch({
body,
value,
setValue,
className,
}: HorizontalSelectProps<SelectValue>) {
const defaultValue = body[0];
const buyValue = body.find((e) => e.code === 'buy');
const sellValue = body.find((e) => e.code === 'sell');
const isBuy = value.code === 'buy';
return (
<button
className={classes(
styles['orders-buy-sell-switch'],
isBuy ? styles['orders-buy-sell-switch_sell'] : undefined,
className,
)}
onClick={() => setValue((isBuy ? sellValue : buyValue) || defaultValue)}
>
<p>To {isBuy ? 'Sell' : 'Buy'} Orders</p>
<ArrowIcon />
</button>
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
import React from 'react';
import { classes, formatTime } from '@/utils/utils';
import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader';
import EmptyMessage from '@/components/UI/EmptyMessage';
import styles from './styles.module.scss';
import { AllTradesProps } from './types';
const AllTrades = ({
setTradesType,
tradesType,
filteredTrades,
tradesLoading,
currencyNames,
}: AllTradesProps) => {
return (
<div className={styles.allTrades}>
<div className={styles.allTrades__header}>
<button
onClick={() => setTradesType('all')}
className={classes(
styles.allTrades__header_btn,
tradesType === 'all' && styles.active,
)}
>
All Trades
</button>
<button
onClick={() => setTradesType('my')}
className={classes(
styles.allTrades__header_btn,
tradesType === 'my' && styles.active,
)}
>
My Trades
</button>
</div>
<div className={styles.allTrades__content}>
<table>
<thead>
<tr>
<th>Price ({currencyNames.secondCurrencyName})</th>
<th>Amount ({currencyNames.firstCurrencyName})</th>
<th>Time</th>
</tr>
</thead>
{!tradesLoading && !!filteredTrades.length && (
<tbody className="orders-scroll">
{filteredTrades.map((trade) => (
<tr key={trade.id}>
<td>
<p
style={{
color: trade.id % 2 === 0 ? '#16D1D6' : '#FF6767',
}}
>
{trade.price}
</p>
</td>
<td>
<p>{trade.amount}</p>
</td>
<td>
<p>{formatTime(trade.timestamp)}</p>
</td>
</tr>
))}
</tbody>
)}
</table>
{!filteredTrades.length && !tradesLoading && <EmptyMessage text="No trades" />}
{tradesLoading && <ContentPreloader style={{ marginTop: 40 }} />}
</div>
</div>
);
};
export default AllTrades;

View file

@ -0,0 +1,116 @@
.allTrades {
max-width: 415px;
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1480px) {
max-width: 340px;
}
&__header {
border-bottom: 1px solid var(--delimiter-color);
display: flex;
align-items: center;
gap: 22px;
padding: 10px;
padding-bottom: 0;
&_btn {
padding-bottom: 7px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
font-size: 16px;
border-bottom: 2px solid transparent;
font-weight: 600;
background-color: transparent !important;
cursor: pointer;
&.active {
border-color: #1f8feb;
}
&:hover {
color: #1f8feb;
}
}
}
&__content {
display: flex;
flex-direction: column;
padding-top: 10px;
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
margin-bottom: 9px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
min-width: 80px;
font-size: 11px;
font-weight: 700;
text-align: start;
color: var(--table-th-color);
&:last-child {
text-align: right;
}
}
}
}
tbody {
height: 29dvh;
min-height: 265px;
max-height: 380px;
display: flex;
flex-direction: column;
overflow: auto;
padding: 10px;
padding-bottom: 20px;
tr {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
&:nth-child(even) {
background-color: var(--table-even-bg);
}
td {
position: relative;
&:last-child {
> p {
text-align: right;
}
}
> p {
min-width: 80px;
width: 100%;
font-size: 12px;
font-weight: 400;
}
}
}
}
}
}
}

View file

@ -0,0 +1,15 @@
import { Trade } from '@/interfaces/responses/trades/GetTradeRes';
import { Dispatch, SetStateAction } from 'react';
type tradeType = 'all' | 'my';
export interface AllTradesProps {
setTradesType: Dispatch<SetStateAction<tradeType>>;
tradesType: tradeType;
filteredTrades: Trade[];
tradesLoading: boolean;
currencyNames: {
firstCurrencyName: string;
secondCurrencyName: string;
};
}

View file

@ -0,0 +1,26 @@
import { classes } from '@/utils/utils';
import React from 'react';
import Image from 'next/image';
import LightningImg from '@/assets/images/UI/lightning.png';
import RocketImg from '@/assets/images/UI/rocket.png';
import styles from './styles.module.scss';
import { BadgeStatusProps } from './types';
function BadgeStatus({ type, icon }: BadgeStatusProps) {
return (
<div className={classes(styles.badge, type === 'high' && styles.high, icon && styles.icon)}>
<Image
className={styles.badge__img}
src={type === 'instant' ? LightningImg : RocketImg}
alt="badge image"
/>
{!icon && (
<span className={styles.badge__text}>
{type === 'instant' ? 'instant' : 'high volume'}
</span>
)}
</div>
);
}
export default BadgeStatus;

View file

@ -0,0 +1,39 @@
.badge {
width: fit-content;
padding: 1px 4px;
padding-right: 8px;
display: flex;
align-items: center;
gap: 2px;
border-radius: 100px;
background: radial-gradient(100% 246.57% at 0% 0%, #a366ff 0%, #601fff 100%);
&.high {
padding: 2px 4px;
background: radial-gradient(100% 188.88% at 0% 0%, #16d1d6 0%, #274cff 100%);
}
&.icon {
min-width: 15px;
height: 15px;
border-radius: 50%;
justify-content: center;
padding: 0;
.badge__img {
width: 11px;
height: 11px;
}
}
&__img {
height: 13px;
width: auto;
}
&__text {
font-size: 10px;
font-weight: 600;
color: #fff;
}
}

View file

@ -0,0 +1,4 @@
export interface BadgeStatusProps {
type?: 'instant' | 'high';
icon?: boolean;
}

View file

@ -6,7 +6,7 @@ import Decimal from 'decimal.js';
import * as echarts from 'echarts';
import CandleRow from '@/interfaces/common/CandleRow';
import testCandles from './testCandles.json';
import styles from './CandleChart.module.scss';
import styles from './styles.module.scss';
const TESTING_MODE = false;
@ -214,7 +214,7 @@ function CandleChart(props: CandleChartProps) {
console.log('option', option);
return (
<div className={styles.candle__chart__wrapper}>
<div className={styles.chart}>
<ReactECharts
option={option}
style={{
@ -229,7 +229,9 @@ function CandleChart(props: CandleChartProps) {
ref={chartRef}
/>
{!candles?.length && isLoaded && <h1>[ Low volume ]</h1>}
{!candles?.length && isLoaded && (
<h1 className={styles.chart__lowVolume}>[ Low volume ]</h1>
)}
</div>
);
}

View file

@ -1,4 +1,4 @@
.candle__chart__wrapper {
.chart {
position: relative;
width: auto;
height: auto;
@ -12,7 +12,7 @@
cursor: crosshair;
}
h1 {
&__lowVolume {
font-size: 72px;
color: var(--font-dimmed-color);
white-space: nowrap;

View file

@ -0,0 +1,52 @@
import LabeledInputProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps';
import { classes, formatDollarValue } from '@/utils/utils';
import { useRef } from 'react';
import Input from '@/components/UI/Input/Input';
import styles from './styles.module.scss';
function LabeledInput(props: LabeledInputProps) {
const labelRef = useRef<HTMLParagraphElement>(null);
const {
label = '',
placeholder = '',
currency = '',
value,
readonly,
usd,
setValue,
invalid,
} = props;
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
if (!readonly && setValue) {
setValue(e.currentTarget.value);
}
};
return (
<div className={styles.labeledInput}>
<h6 className={styles.labeledInput__label}>{label}</h6>
<div className={classes(styles.labeledInput__wrapper, invalid && styles.invalid)}>
<Input
bordered
placeholder={placeholder}
value={value}
readOnly={readonly}
onInput={handleInput}
/>
{usd && (
<div className={styles.labeledInput__value}>
<p>~${formatDollarValue(usd)}</p>
</div>
)}
<div className={styles.labeledInput__currency} ref={labelRef}>
<p>{currency}</p>
</div>
</div>
</div>
);
}
export default LabeledInput;

View file

@ -0,0 +1,62 @@
.labeledInput {
display: flex;
flex-direction: column;
gap: 8px;
&__label {
font-size: 11px;
font-family: 700;
color: var(--table-th-color);
}
&__wrapper {
width: 100%;
position: relative;
background-color: var(--bordered-input-bg);
border: 1px solid var(--window-border-color);
border-radius: 8px;
display: flex;
overflow: hidden;
&.invalid {
border-color: #ff6767;
}
input {
width: 100%;
padding: 13px 15px;
background-color: transparent;
border: none;
font-size: 16px;
font-weight: 400;
}
}
&__value {
padding-right: 10px;
display: flex;
align-items: center;
> p {
color: var(--table-th-color);
font-size: 12px;
font-weight: 400;
}
}
&__currency {
min-width: 82px;
max-width: 150px;
padding: 0 15px;
background-color: var(--dex-input-currency);
display: flex;
align-items: center;
justify-content: center;
> p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}

View file

@ -1,26 +1,22 @@
import { Store } from '@/store/store-reducer';
import { createOrder } from '@/utils/methods';
import { ChangeEvent, useContext, useRef, useState } from 'react';
import Input from '@/components/UI/Input/Input';
import { ChangeEvent, useContext, useState } from 'react';
import RangeInput from '@/components/UI/RangeInput/RangeInput';
import ConnectButton from '@/components/UI/ConnectButton/ConnectButton';
import Button from '@/components/UI/Button/Button';
import { useRouter } from 'next/router';
import { classes, formatDollarValue } from '@/utils/utils';
import { classes } from '@/utils/utils';
import InputPanelItemProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps';
import LabeledInputProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps';
import CreateOrderData from '@/interfaces/fetch-data/create-order/CreateOrderData';
import Decimal from 'decimal.js';
import Alert from '@/components/UI/Alert/Alert';
import infoIcon from '@/assets/images/UI/info_alert_icon.svg';
import Image from 'next/image';
import styles from './InputPanelItem.module.scss';
import { useAlert } from '@/hook/useAlert';
import styles from './styles.module.scss';
import LabeledInput from './components/LabeledInput';
function InputPanelItem(props: InputPanelItemProps) {
const { state } = useContext(Store);
const router = useRouter();
const {
priceState = '',
amountState = '',
@ -29,71 +25,32 @@ function InputPanelItem(props: InputPanelItemProps) {
buySellState = buySellValues[0],
setPriceFunction,
setAmountFunction,
setAlertState,
setAlertSubtitle,
setRangeInputValue,
rangeInputValue = '50',
firstCurrencyName = '',
secondCurrencyName = '',
balance = 0,
amountValid,
priceValid,
totalValid,
totalUsd,
scrollToOrderList,
currencyNames,
} = props;
const { state } = useContext(Store);
const router = useRouter();
const { setAlertState, setAlertSubtitle } = useAlert();
const [creatingState, setCreatingState] = useState(false);
const { firstCurrencyName, secondCurrencyName } = currencyNames;
const [hasImmediateMatch, setHasImmediateMatch] = useState(false);
function LabeledInput(props: LabeledInputProps) {
const labelRef = useRef<HTMLParagraphElement>(null);
const {
label = '',
placeholder = '',
currency = '',
value,
readonly,
usd,
setValue,
invalid,
} = props;
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
if (!readonly && setValue) {
setValue(e.currentTarget.value);
}
};
return (
<div
className={classes(styles.labeled_input, invalid && styles.labeled_input__invalid)}
>
<h6>{label}</h6>
<div>
<Input
bordered
placeholder={placeholder}
value={value}
readOnly={readonly}
onInput={handleInput}
/>
{usd && (
<div className={styles.labeled_input__value}>
<p>~${formatDollarValue(usd)}</p>
</div>
)}
<div className={styles.labeled_input__currency} ref={labelRef}>
<p>{currency}</p>
</div>
</div>
</div>
);
}
const isBuy = buySellState?.code === 'buy';
function resetForm() {
setPriceFunction('');
setAmountFunction('');
setRangeInputValue('50');
}
async function postOrder() {
const price = new Decimal(priceState);
const amount = new Decimal(amountState);
@ -122,6 +79,8 @@ function InputPanelItem(props: InputPanelItemProps) {
if (result.data?.immediateMatch) {
setHasImmediateMatch(true);
}
resetForm();
} else {
setAlertState('error');
if (result.data === 'Same order') {
@ -139,35 +98,30 @@ function InputPanelItem(props: InputPanelItemProps) {
function onRangeInput(e: ChangeEvent<HTMLInputElement>) {
setRangeInputValue(e.target.value);
if (balance) {
if (balance > 0) {
const rangeValue = new Decimal(e.target.value || '0');
const balanceDecimal = new Decimal(balance || '0');
const balanceDecimal = new Decimal(balance);
const calculatedAmount = balanceDecimal.mul(rangeValue.div(100)).toString();
setAmountFunction(calculatedAmount || '');
}
}
let buttonText;
if (creatingState) {
buttonText = 'Creating...';
} else {
buttonText = 'Create Order';
}
const buttonText = creatingState ? 'Creating...' : 'Create Order';
const isButtonDisabled = !priceValid || !amountValid || !totalValid || creatingState;
return (
<div className={styles.input_panel__item}>
<div className={styles.inputPanel}>
{hasImmediateMatch && (
<Alert
type="custom"
customContent={
<div className={styles.apply__alert}>
<div className={styles.applyAlert}>
<Image src={infoIcon} alt="success" width={64} height={64} />
<div className={styles.apply__alert__content}>
<div className={styles.applyAlert__content}>
<h2>Apply the order</h2>
<p>You have to apply the order</p>
<Button
className={styles.apply__alert__button}
className={styles.applyAlert__button}
onClick={() => {
scrollToOrderList();
setHasImmediateMatch(false);
@ -182,19 +136,17 @@ function InputPanelItem(props: InputPanelItemProps) {
/>
)}
<div className={styles.input_panel__item_header}>
<h5>
<div className={styles.inputPanel__header}>
<h5 className={styles.title}>
{isBuy ? 'Buy' : 'Sell'} {secondCurrencyName}
</h5>
<div className={styles.input_panel__fees}>
<p>
Fee: <span>0.01 Zano</span>
</p>
</div>
<p className={styles.inputPanel__fees}>
Fee: <span>0.01 Zano</span>
</p>
</div>
<div className={styles.input_panel__item_body}>
<div className={styles.inputPanel__body}>
{LabeledInput({
value: priceState,
setValue: setPriceFunction,
@ -203,6 +155,7 @@ function InputPanelItem(props: InputPanelItemProps) {
label: 'Price',
invalid: !!priceState && !priceValid,
})}
{LabeledInput({
value: amountState,
setValue: setAmountFunction,
@ -211,7 +164,9 @@ function InputPanelItem(props: InputPanelItemProps) {
label: 'Amount',
invalid: !!amountState && !amountValid,
})}
<RangeInput value={rangeInputValue} onInput={onRangeInput} />
{LabeledInput({
value: totalState,
setValue: () => undefined,
@ -224,14 +179,22 @@ function InputPanelItem(props: InputPanelItemProps) {
})}
{state.wallet?.connected ? (
<Button
disabled={!priceValid || !amountValid || !totalValid}
disabled={isButtonDisabled}
onClick={postOrder}
className={isBuy ? styles.buy_btn : styles.sell_btn}
className={classes(
styles.inputPanel__body_btn,
isBuy ? styles.buy : styles.sell,
)}
>
{buttonText}
</Button>
) : (
<ConnectButton className={isBuy ? styles.buy_btn : styles.sell_btn} />
<ConnectButton
className={classes(
styles.inputPanel__body_btn,
isBuy ? styles.buy : styles.sell,
)}
/>
)}
</div>
</div>

View file

@ -0,0 +1,94 @@
.inputPanel {
width: 100%;
padding: 15px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
.title {
font-size: 16px;
font-weight: 600;
}
}
&__fees {
color: var(--table-th-color);
font-weight: 400;
font-size: 12px;
span {
font-weight: 400;
font-size: 12px;
}
}
&__body {
display: flex;
flex-direction: column;
gap: 10px;
&_btn {
margin-top: 10px;
&.buy {
background-color: #16d1d6;
&:hover {
background-color: #45dade;
}
}
&.sell {
background-color: #ff6767;
&:hover {
background-color: #ff8585;
}
}
}
}
}
.applyAlert {
display: flex;
gap: 20px;
align-items: center;
&__content {
display: flex;
flex-direction: column;
gap: 10px;
}
&__button {
max-width: 125px;
background-color: var(--alert-btn-bg);
color: #1f8feb;
padding: 7px 32px;
font-size: 12px;
font-weight: 500;
&:hover {
background-color: var(--alert-btn-hover);
}
}
h2 {
font-size: 16px;
font-weight: 600;
}
p {
font-size: 14px;
opacity: 0.7;
margin-bottom: 5px;
}
}

View file

@ -0,0 +1,41 @@
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import { useState } from 'react';
import { ReactComponent as ConnectionIcon } from '@/assets/images/UI/connection.svg';
import styles from './styles.module.scss';
import { MatrixConnectionBadgeProps } from './types';
function MatrixConnectionBadge({
userAdress,
userAlias,
matrixAddresses,
}: MatrixConnectionBadgeProps) {
const hasConnection = (address: string) =>
matrixAddresses.some((item) => item.address === address && item.registered);
const [connectionTooltip, setConnectionTooltip] = useState(false);
return userAdress && hasConnection(userAdress) ? (
<div className={styles.badge}>
<a
href={`https://matrix.to/#/@${userAlias}:zano.org`}
target="_blank"
onMouseEnter={() => setConnectionTooltip(true)}
onMouseLeave={() => setConnectionTooltip(false)}
className={styles.badge__link}
>
<ConnectionIcon />
</a>
<Tooltip
className={styles.badge__tooltip}
arrowClass={styles.badge__tooltip_arrow}
shown={connectionTooltip}
>
<p className={styles.badge__tooltip_text}>Matrix connection</p>
</Tooltip>
</div>
) : (
<></>
);
}
export default MatrixConnectionBadge;

View file

@ -0,0 +1,28 @@
.badge {
position: relative;
&__link {
margin-top: 4px;
cursor: pointer;
}
&__tooltip {
position: absolute;
top: 30px;
left: 50%;
transform: translateX(-50%);
background-color: var(--trade-table-tooltip);
font-size: 12px;
z-index: 9999;
&_text {
font-size: 12px !important;
}
&_arrow {
border-radius: 2px;
left: 50%;
background-color: var(--trade-table-tooltip) !important;
}
}
}

View file

@ -0,0 +1,7 @@
import MatrixAddress from '@/interfaces/common/MatrixAddress';
export interface MatrixConnectionBadgeProps {
userAdress?: string;
userAlias?: string;
matrixAddresses: MatrixAddress[];
}

View file

@ -0,0 +1,52 @@
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import { useState } from 'react';
import styles from './styles.module.scss';
import { OrderRowTooltipCellProps } from './types';
function OrderRowTooltipCell({
style,
children,
sideText,
sideTextColor,
noTooltip,
}: OrderRowTooltipCellProps) {
const [showTooltip, setShowTooltip] = useState(false);
const tooltipText = `${children}${sideText ? ` ~${sideText}` : ''}`;
const isLongContent = tooltipText.length > 14;
return (
<td className={styles.row}>
<p
style={style}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
{children}
{sideText && (
<span
style={{
fontSize: '15px',
margin: 0,
color: sideTextColor || 'var(--font-dimmed-color)',
}}
>
{sideText}
</span>
)}
</p>
{isLongContent && !noTooltip && (
<Tooltip
className={styles.tooltip}
arrowClass={styles.tooltip__arrow}
shown={showTooltip}
>
{tooltipText}
</Tooltip>
)}
</td>
);
}
export default OrderRowTooltipCell;

View file

@ -0,0 +1,28 @@
.row {
position: relative;
&:last-child {
> p {
text-align: right;
}
}
> p {
min-width: 80px;
width: 100%;
font-size: 12px;
font-weight: 400;
}
}
.tooltip {
position: absolute;
top: 30px;
left: 20%;
transform: translateX(-50%);
background-color: var(--trade-table-tooltip);
&__arrow {
background-color: var(--trade-table-tooltip);
}
}

View file

@ -0,0 +1,9 @@
import { ReactNode } from 'react';
export interface OrderRowTooltipCellProps {
style?: React.CSSProperties;
children: string | ReactNode;
sideText?: string;
sideTextColor?: string;
noTooltip?: boolean;
}

View file

@ -0,0 +1,54 @@
import React from 'react';
import { classes, notationToString } from '@/utils/utils';
import { nanoid } from 'nanoid';
import Decimal from 'decimal.js';
import styles from './styles.module.scss';
import { OrdersRowProps } from './types';
import OrderRowTooltipCell from '../../../OrderRowTooltipCell';
function OrdersRow({
orderData,
percentage,
takeOrderClick,
setOrdersInfoTooltip,
}: OrdersRowProps) {
const e = orderData || {};
const totalDecimal = new Decimal(e.left).mul(new Decimal(e.price));
return (
<tr
onMouseEnter={() => setOrdersInfoTooltip(e)}
onClick={(event) => takeOrderClick(event, e)}
className={classes(styles.row, e.type === 'sell' && styles.sell_section)}
style={{ '--line-width': `${percentage}%` } as React.CSSProperties}
key={nanoid(16)}
>
<OrderRowTooltipCell
style={{
color: e.type === 'buy' ? '#16D1D6' : '#FF6767',
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxWidth: 'max-content',
}}
>
{notationToString(e.price)}
</OrderRowTooltipCell>
<OrderRowTooltipCell>{notationToString(e.amount)}</OrderRowTooltipCell>
<OrderRowTooltipCell
noTooltip
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
maxWidth: 'max-content',
}}
>
{notationToString(totalDecimal.toString())}
</OrderRowTooltipCell>
</tr>
);
}
export default OrdersRow;

View file

@ -0,0 +1,48 @@
.row {
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
&:nth-child(even) {
background-color: var(--table-even-bg);
}
&::after {
content: '';
pointer-events: none;
position: absolute;
z-index: 1;
right: 0;
top: 0;
width: var(--line-width, 0%);
height: 100%;
background: #16d1d61a;
}
&.sell_section {
&::after {
background: #ff67671a;
}
}
td {
position: relative;
&:last-child {
> p {
text-align: right;
}
}
> p {
min-width: 80px;
width: 100%;
font-size: 12px;
font-weight: 400;
}
}
}

View file

@ -0,0 +1,14 @@
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { Dispatch, SetStateAction } from 'react';
export interface OrdersRowProps {
orderData: PageOrderData;
percentage: number;
takeOrderClick: (
_event:
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
| React.MouseEvent<HTMLTableRowElement, MouseEvent>,
_e: PageOrderData,
) => void;
setOrdersInfoTooltip: Dispatch<SetStateAction<PageOrderData | null>>;
}

View file

@ -0,0 +1,198 @@
import React, { useRef, useState } from 'react';
import { classes, cutAddress, formatDollarValue, notationToString } from '@/utils/utils';
import { nanoid } from 'nanoid';
import Decimal from 'decimal.js';
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader';
import { buySellValues } from '@/constants';
import EmptyMessage from '@/components/UI/EmptyMessage';
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import useMouseLeave from '@/hook/useMouseLeave';
import OrdersRow from './components/OrdersRow';
import styles from './styles.module.scss';
import BadgeStatus from '../BadgeStatus';
import { OrdersPoolProps } from './types';
const OrdersPool = (props: OrdersPoolProps) => {
const {
ordersBuySell,
setOrdersBuySell,
currencyNames,
ordersLoading,
filteredOrdersHistory,
secondAssetUsdPrice,
takeOrderClick,
} = props;
const ordersInfoRef = useRef<HTMLTableSectionElement | null>(null);
const { firstCurrencyName, secondCurrencyName } = currencyNames;
const [infoTooltipPos, setInfoTooltipPos] = useState({ x: 0, y: 0 });
const [ordersInfoTooltip, setOrdersInfoTooltip] = useState<PageOrderData | null>(null);
const moveInfoTooltip = (event: React.MouseEvent) => {
setInfoTooltipPos({ x: event.clientX, y: event.clientY });
};
useMouseLeave(ordersInfoRef, () => setOrdersInfoTooltip(null));
return (
<>
<div className={styles.ordersPool}>
<div className={styles.ordersPool__header}>
<h5 className={styles.ordersPool__header_title}>Orders pool</h5>
<div className={styles.ordersPool__header_type}>
<button
onClick={() => setOrdersBuySell(buySellValues[0])}
className={classes(
styles.btn,
styles.all,
ordersBuySell.code === 'all' && styles.selected,
)}
></button>
<button
onClick={() => setOrdersBuySell(buySellValues[1])}
className={classes(
styles.btn,
styles.buy,
ordersBuySell.code === 'buy' && styles.selected,
)}
>
B
</button>
<button
onClick={() => setOrdersBuySell(buySellValues[2])}
className={classes(
styles.btn,
styles.sell,
ordersBuySell.code === 'sell' && styles.selected,
)}
>
S
</button>
</div>
</div>
<div className={styles.ordersPool__content}>
<table>
<thead>
<tr>
<th>Price ({secondCurrencyName})</th>
<th>Amount ({firstCurrencyName})</th>
<th>Total ({secondCurrencyName})</th>
</tr>
</thead>
{!ordersLoading && !!filteredOrdersHistory.length && (
<tbody
ref={ordersInfoRef}
onMouseMove={moveInfoTooltip}
onMouseLeave={() => setOrdersInfoTooltip(null)}
className="orders-scroll"
>
{filteredOrdersHistory?.map((e) => {
const maxValue = Math.max(
...filteredOrdersHistory.map((order) =>
parseFloat(String(order.left)),
),
);
const percentage = (
(parseFloat(String(e.left)) / maxValue) *
100
).toFixed(2);
return (
<OrdersRow
orderData={e}
percentage={Number(percentage)}
key={nanoid(16)}
takeOrderClick={takeOrderClick}
setOrdersInfoTooltip={setOrdersInfoTooltip}
/>
);
})}
</tbody>
)}
</table>
{!filteredOrdersHistory.length && !ordersLoading && (
<EmptyMessage text="No orders" />
)}
{ordersLoading && <ContentPreloader style={{ marginTop: 40 }} />}
</div>
</div>
{/* Order tooltip */}
{ordersInfoTooltip &&
(() => {
const totalDecimal = new Decimal(ordersInfoTooltip?.left).mul(
new Decimal(ordersInfoTooltip?.price),
);
const totalValue = secondAssetUsdPrice
? totalDecimal.mul(secondAssetUsdPrice).toFixed(2)
: undefined;
return (
<Tooltip
key={nanoid(16)}
className={styles.tooltip}
arrowClass={styles.tooltip__arrow}
style={{
left: infoTooltipPos.x,
top: infoTooltipPos.y + 20,
}}
shown
>
<div>
<h6>Alias</h6>
<p>
@{cutAddress(ordersInfoTooltip?.user?.alias || 'no alias', 12)}{' '}
{ordersInfoTooltip?.isInstant && (
<BadgeStatus type="instant" icon />
)}
</p>
<h6>Price ({secondCurrencyName})</h6>
<p
style={{
color:
ordersInfoTooltip?.type === 'buy'
? '#16D1D6'
: '#FF6767',
}}
>
{notationToString(ordersInfoTooltip?.price)}
</p>
<span>
~
{secondAssetUsdPrice && ordersInfoTooltip?.price !== undefined
? (() => {
const total =
secondAssetUsdPrice * ordersInfoTooltip.price;
const formatted =
ordersInfoTooltip.price < 0.9
? `$${total.toFixed(5)}`
: `$${total.toFixed(2)}`;
return formatted;
})()
: 'undefined'}
</span>
<h6>Amount ({firstCurrencyName})</h6>
<p>{notationToString(ordersInfoTooltip?.amount)}</p>
<h6>Total ({secondCurrencyName})</h6>
<p>{notationToString(totalDecimal.toString())}</p>
<span>
~{' '}
{totalValue ? `$${formatDollarValue(totalValue)}` : 'undefined'}
</span>
</div>
</Tooltip>
);
})()}
</>
);
};
export default OrdersPool;

View file

@ -0,0 +1,151 @@
.ordersPool {
max-width: 415px;
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1480px) {
max-width: 340px;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px;
padding-bottom: 10px;
border-bottom: 1px solid var(--delimiter-color);
&_title {
font-size: 18px;
font-weight: 600;
}
&_type {
display: flex;
align-items: center;
gap: 8px;
.btn {
cursor: pointer;
width: 20px;
height: 20px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
transition: 0.3s opacity ease;
color: #ffffff;
&.selected,
&:hover {
opacity: 80%;
}
&.all {
background: linear-gradient(to left, #ff6767 50%, #16d1d6 50%);
}
&.buy {
background-color: #16d1d6;
}
&.sell {
background-color: #ff6767;
}
}
}
}
&__content {
display: flex;
flex-direction: column;
padding-top: 10px;
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
margin-bottom: 9px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
font-size: 11px;
font-weight: 700;
text-align: start;
color: var(--table-th-color);
min-width: 80px;
&:last-child {
text-align: right;
}
}
}
}
tbody {
height: 29dvh;
min-height: 265px;
max-height: 380px;
display: flex;
flex-direction: column;
overflow: auto;
padding-bottom: 20px;
padding: 10px;
}
}
}
}
.tooltip {
pointer-events: none;
position: fixed;
border: 1px solid var(--dex-tooltip-border-color);
min-width: 140px;
max-width: 180px;
padding: 10px;
transform: translateX(-50%);
background-color: var(--dex-tooltip-bg);
&__arrow {
border-top: 1px solid var(--dex-tooltip-border-color);
background-color: var(--dex-tooltip-bg) !important;
}
h6 {
color: var(--table-th-color);
margin-top: 12px;
font-size: 11px;
font-weight: 700;
&:first-child {
margin-top: 0;
}
}
p {
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
gap: 5px;
margin-top: 6px;
}
span {
margin-top: 5px;
display: block;
color: #8d95ae;
font-size: 11px;
font-weight: 400;
}
}

View file

@ -0,0 +1,21 @@
import { Dispatch, SetStateAction } from 'react';
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
export interface OrdersPoolProps {
ordersBuySell: SelectValue;
setOrdersBuySell: Dispatch<SetStateAction<SelectValue>>;
currencyNames: {
firstCurrencyName: string;
secondCurrencyName: string;
};
ordersLoading: boolean;
filteredOrdersHistory: PageOrderData[];
secondAssetUsdPrice: number | undefined;
takeOrderClick: (
_event:
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
| React.MouseEvent<HTMLTableRowElement, MouseEvent>,
_e: PageOrderData,
) => void;
}

View file

@ -0,0 +1,32 @@
import StatItemProps from '@/interfaces/props/pages/dex/trading/StatItemProps';
import { classes } from '@/utils/utils';
import styles from './styles.module.scss';
function StatItem({ Img, title, value, className, coefficient }: StatItemProps) {
return (
<div className={classes(styles.statItem, className)}>
<div className={styles.statItem__nav}>
<Img />
<p className={styles.statItem__nav_title}>{title}</p>
</div>
<div className={styles.statItem__content}>
<p className={styles.statItem__content_val}>{value}</p>
{coefficient !== undefined && (
<p
className={classes(
styles.statItem__content_coefficient,
coefficient >= 0 ? styles.green : styles.red,
)}
>
{coefficient >= 0 ? '+' : ''}
{coefficient?.toFixed(2)}%
</p>
)}
</div>
</div>
);
}
export default StatItem;

View file

@ -0,0 +1,44 @@
.statItem {
display: flex;
flex-direction: column;
gap: 6px;
&__nav {
display: flex;
align-items: center;
gap: 5px;
&_title {
color: var(--footer-selected-link);
white-space: nowrap;
font-size: 14px;
font-weight: 700;
}
}
&__content {
display: flex;
align-items: center;
gap: 5px;
&_val {
white-space: nowrap;
font-size: 14px;
font-weight: 400;
}
&_coefficient {
white-space: nowrap;
font-size: 14px;
font-weight: 400;
&.green {
color: #16d1d6;
}
&.red {
color: #ff6767;
}
}
}
}

View file

@ -0,0 +1,23 @@
import { shortenAddress } from '@/utils/utils';
import Link from 'next/link';
import CurrencyIcon from '../CurrencyIcon';
import styles from './styles.module.scss';
import { AssetRowProps } from './types';
const AssetRow = ({ name, link, id, code }: AssetRowProps) => (
<div className={styles.asset}>
<p className={styles.asset__name}>
<CurrencyIcon code={code} size={16} /> {name}:
</p>
<Link
className={styles.asset__address}
rel="noopener noreferrer"
target="_blank"
href={link}
>
{shortenAddress(id)}
</Link>
</div>
);
export default AssetRow;

View file

@ -0,0 +1,19 @@
.asset {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
&__name {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
font-weight: 400;
}
&__address {
font-size: 14px;
font-weight: 400;
}
}

View file

@ -0,0 +1,6 @@
export interface AssetRowProps {
name: string;
link: string;
id: string;
code: string | undefined;
}

View file

@ -0,0 +1,8 @@
import Image from 'next/image';
import { CurrencyIconProps } from './types';
const CurrencyIcon = ({ code, size = 50 }: CurrencyIconProps) => (
<Image width={size} height={size} src={`/currencies/trade_${code}.svg`} alt="currency" />
);
export default CurrencyIcon;

View file

@ -0,0 +1,4 @@
export interface CurrencyIconProps {
code: string | undefined;
size?: number;
}

View file

@ -0,0 +1,127 @@
import { ReactComponent as ClockIcon } from '@/assets/images/UI/clock_icon.svg';
import { ReactComponent as UpIcon } from '@/assets/images/UI/up_icon.svg';
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 { tradingKnownCurrencies, roundTo, notationToString } from '@/utils/utils';
import styles from './styles.module.scss';
import StatItem from '../StatItem';
import { TradingHeaderProps } from './types';
import CurrencyIcon from './components/CurrencyIcon';
import AssetRow from './components/AssetRow';
const getCurrencyCode = (code?: string) =>
tradingKnownCurrencies.includes(code || '') ? code : 'tsds';
const TradingHeader = ({
pairStats,
pairRateUsd,
firstAssetLink,
secondAssetLink,
firstAssetId,
secondAssetId,
pairData,
}: TradingHeaderProps) => {
const currencyNames = {
firstCurrencyName: pairData?.first_currency?.name || '',
secondCurrencyName: pairData?.second_currency?.name || '',
};
const { firstCurrencyName, secondCurrencyName } = currencyNames;
const imgCode = getCurrencyCode(pairData?.first_currency?.code || '');
const imgCode2 = getCurrencyCode(pairData?.second_currency?.code || '');
const coefficient = pairStats?.coefficient || 0;
const coefficientOutput =
parseFloat(coefficient?.toFixed(2) || '0') === -100
? -99.99
: parseFloat(coefficient?.toFixed(2) || '0');
const stats = [
{
Img: ClockIcon,
title: '24h change',
value: `${roundTo(notationToString(pairStats?.rate || 0), 8)} ${secondCurrencyName}`,
coefficient: coefficientOutput,
},
{
Img: UpIcon,
title: '24h high',
value: `${roundTo(notationToString(pairStats?.high || 0), 8)} ${secondCurrencyName}`,
},
{
Img: DownIcon,
title: '24h low',
value: `${roundTo(notationToString(pairStats?.low || 0), 8)} ${secondCurrencyName}`,
},
{
Img: VolumeIcon,
title: '24h volume',
value: `${roundTo(notationToString(pairStats?.volume || 0), 8)} ${secondCurrencyName}`,
},
];
return (
<div className={styles.header}>
<div className={styles.header__currency}>
<div className={styles.header__currency_icon}>
<CurrencyIcon code={imgCode} />
</div>
<div className={styles.header__currency_item}>
<p className={styles.currencyName}>
{!pairData ? (
'...'
) : (
<>
{firstCurrencyName}
<span>/{secondCurrencyName}</span>
</>
)}
</p>
<div className={styles.price}>
<p className={styles.price__secondCurrency}>
{notationToString(pairStats?.rate || 0)} {secondCurrencyName}
</p>
{pairRateUsd && <p className={styles.price__usd}>~ ${pairRateUsd}</p>}
</div>
</div>
</div>
<div className={styles.header__stats}>
{pairData && firstAssetLink && secondAssetLink && (
<div className={styles.header__stats_assets}>
<AssetRow
name={firstCurrencyName}
link={firstAssetLink}
id={firstAssetId || ''}
code={imgCode}
/>
<AssetRow
name={secondCurrencyName}
link={secondAssetLink}
id={secondAssetId || ''}
code={imgCode2}
/>
</div>
)}
{stats.map(({ Img, title, value, coefficient }) => (
<StatItem
key={title}
className={styles.header__stats_item}
Img={Img}
title={title}
value={value}
coefficient={coefficient}
/>
))}
</div>
<BackButton />
</div>
);
};
export default TradingHeader;

View file

@ -0,0 +1,78 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 25px;
position: relative;
border: none;
&__currency {
display: flex;
align-items: center;
gap: 12px;
&_icon {
min-width: 48px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--icon-bg-color);
border-radius: 50%;
> img {
width: 26px;
height: auto;
}
}
&_item {
display: flex;
flex-direction: column;
justify-content: space-between;
.currencyName {
font-size: 18px;
font-weight: 600;
span {
color: var(--footer-selected-link);
}
}
.price {
display: flex;
align-items: center;
gap: 5px;
&__secondCurrency {
font-weight: 400;
font-size: 14px;
}
&__usd {
color: var(--footer-selected-link);
font-size: 12px;
font-weight: 400;
}
}
}
}
&__stats {
display: flex;
flex-wrap: nowrap;
gap: 20px;
&_assets {
display: flex;
flex-direction: column;
gap: 7px;
}
&_item {
padding-left: 20px;
border-left: 1px solid var(--delimiter-color);
}
}
}

View file

@ -0,0 +1,12 @@
import PairData from '@/interfaces/common/PairData';
import { PairStats } from '@/interfaces/responses/orders/GetPairStatsRes';
export interface TradingHeaderProps {
pairStats: PairStats | null;
pairRateUsd: string | undefined;
firstAssetLink?: string;
secondAssetLink?: string;
firstAssetId?: string | null;
secondAssetId?: string | null;
pairData: PairData | null;
}

View file

@ -0,0 +1,231 @@
import React, { useContext, useState } from 'react';
import { cutAddress, formatDollarValue, notationToString } from '@/utils/utils';
import { nanoid } from 'nanoid';
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import Decimal from 'decimal.js';
import { confirmIonicSwap, ionicSwap } from '@/utils/wallet';
import { applyOrder, confirmTransaction } from '@/utils/methods';
import { updateAutoClosedNotification } from '@/store/actions';
import Link from 'next/link';
import { Store } from '@/store/store-reducer';
import { useAlert } from '@/hook/useAlert';
import OrderRowTooltipCell from '../../../OrderRowTooltipCell';
import MatrixConnectionBadge from '../../../MatrixConnectionBadge';
import styles from '../../styles.module.scss';
import { MyOrdersApplyRowProps } from './types';
import BadgeStatus from '../../../BadgeStatus';
function MyOrdersApplyRow(props: MyOrdersApplyRowProps) {
const {
orderData,
fetchUser,
matrixAddresses,
secondAssetUsdPrice,
updateOrders,
updateUserOrders,
fetchTrades,
pairData,
userOrders,
} = props;
const e = orderData || {};
const { state, dispatch } = useContext(Store);
const { setAlertState, setAlertSubtitle } = useAlert();
const [applyingState, setApplyingState] = useState(false);
const connectedOrder = userOrders.find((order) => order.id === e.connected_order_id);
const totalDecimal = new Decimal(e.left).mul(new Decimal(e.price));
const totalValue = secondAssetUsdPrice
? totalDecimal.mul(secondAssetUsdPrice).toFixed(2)
: undefined;
const [showTooltip, setShowTooltip] = useState(false);
async function applyClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
event.preventDefault();
if (e.id) {
updateAutoClosedNotification(dispatch, [
...state.closed_notifications,
parseInt(e.id, 10),
]);
}
function alertErr(subtitle: string) {
setAlertState('error');
setAlertSubtitle(subtitle);
setTimeout(() => {
setAlertState(null);
setAlertSubtitle('');
}, 3000);
}
setApplyingState(true);
interface SwapOperationResult {
success: boolean;
message?: string;
errorCode?: number;
data?: unknown;
}
let result: SwapOperationResult | null = null;
await (async () => {
if (e.transaction) {
if (!e.hex_raw_proposal) {
alertErr('Invalid transaction data received');
return;
}
console.log(e.hex_raw_proposal);
const confirmSwapResult = await confirmIonicSwap(e.hex_raw_proposal);
console.log(confirmSwapResult);
if (confirmSwapResult.data?.error?.code === -7) {
alertErr('Insufficient funds');
return;
}
if (!confirmSwapResult.data?.result) {
alertErr('Companion responded with an error');
return;
}
result = await confirmTransaction(e.id);
} else {
const firstCurrencyId = pairData?.first_currency.asset_id;
const secondCurrencyId = pairData?.second_currency.asset_id;
console.log(firstCurrencyId, secondCurrencyId);
if (!(firstCurrencyId && secondCurrencyId)) {
alertErr('Invalid transaction data received');
return;
}
if (!connectedOrder) return;
const leftDecimal = new Decimal(e.left);
const priceDecimal = new Decimal(e.price);
const params = {
destinationAssetID: e.type === 'buy' ? secondCurrencyId : firstCurrencyId,
destinationAssetAmount: notationToString(
e.type === 'buy'
? leftDecimal.mul(priceDecimal).toString()
: leftDecimal.toString(),
),
currentAssetID: e.type === 'buy' ? firstCurrencyId : secondCurrencyId,
currentAssetAmount: notationToString(
e.type === 'buy'
? leftDecimal.toString()
: leftDecimal.mul(priceDecimal).toString(),
),
destinationAddress: e.user.address,
};
console.log(params);
const createSwapResult = await ionicSwap(params);
console.log(createSwapResult);
const hex = createSwapResult?.data?.result?.hex_raw_proposal;
if (createSwapResult?.data?.error?.code === -7) {
alertErr('Insufficient funds');
return;
}
if (!hex) {
alertErr('Companion responded with an error');
return;
}
result = await applyOrder({
...e,
hex_raw_proposal: hex,
});
}
})();
setApplyingState(false);
if (!result) {
return;
}
if (!(result as { success: boolean }).success) {
alertErr('Server responded with an error');
return;
}
await updateOrders();
await updateUserOrders();
await fetchUser();
await fetchTrades();
}
return (
<tr key={nanoid(16)}>
<td>
<p className={styles.alias}>
<span
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
className={styles.alias__text}
>
@{cutAddress(e.user.alias, 12) || 'no alias'}
</span>
<MatrixConnectionBadge
userAdress={e.user.address}
userAlias={e.user.alias}
matrixAddresses={matrixAddresses}
/>
{e.isInstant && (
<div style={{ marginLeft: 2 }}>
<BadgeStatus type="instant" icon />
</div>
)}
</p>
{(e.isInstant || e.transaction) && <BadgeStatus type="instant" />}
{e.user?.alias.length > 12 && (
<Tooltip
className={styles.tooltip}
arrowClass={styles.tooltip__arrow}
shown={showTooltip}
>
<p className={styles.tooltip__text}>{e.user?.alias}</p>
</Tooltip>
)}
</td>
<OrderRowTooltipCell style={{ color: e.type === 'buy' ? '#16D1D6' : '#FF6767' }}>
{notationToString(e.price)}
</OrderRowTooltipCell>
<OrderRowTooltipCell>{notationToString(e.left)}</OrderRowTooltipCell>
<OrderRowTooltipCell
noTooltip
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
>
{notationToString(totalDecimal.toString())}{' '}
<span>~ ${totalValue && formatDollarValue(totalValue)}</span>
</OrderRowTooltipCell>
<td></td>
<td>
<Link href="/" onClick={applyClick}>
{applyingState ? 'Process' : 'Apply'}
</Link>
</td>
</tr>
);
}
export default MyOrdersApplyRow;

View file

@ -0,0 +1,16 @@
import ApplyTip from '@/interfaces/common/ApplyTip';
import MatrixAddress from '@/interfaces/common/MatrixAddress';
import OrderRow from '@/interfaces/common/OrderRow';
import PairData from '@/interfaces/common/PairData';
export interface MyOrdersApplyRowProps {
orderData: ApplyTip;
secondAssetUsdPrice: number | undefined;
updateOrders: () => Promise<void>;
updateUserOrders: () => Promise<void>;
fetchUser: () => Promise<boolean>;
fetchTrades: () => Promise<void>;
matrixAddresses: MatrixAddress[];
pairData: PairData | null;
userOrders: OrderRow[];
}

View file

@ -0,0 +1,140 @@
import Decimal from 'decimal.js';
import { nanoid } from 'nanoid';
import { useContext, useState } from 'react';
import { cutAddress, formatDollarValue, notationToString } from '@/utils/utils';
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import Link from 'next/link';
import { cancelOrder } from '@/utils/methods';
import { Store } from '@/store/store-reducer';
import { useAlert } from '@/hook/useAlert';
import BadgeStatus from '../../../BadgeStatus';
import styles from '../../styles.module.scss';
import MatrixConnectionBadge from '../../../MatrixConnectionBadge';
import OrderRowTooltipCell from '../../../OrderRowTooltipCell';
import { MyOrdersRowProps } from './types';
function MyOrdersRow(props: MyOrdersRowProps) {
const {
orderData,
secondAssetUsdPrice,
fetchUser,
updateOrders,
updateUserOrders,
matrixAddresses,
applyTips,
} = props;
const e = orderData || {};
const { state } = useContext(Store);
const { setAlertState, setAlertSubtitle } = useAlert();
const [cancellingState, setCancellingState] = useState(false);
const totalDecimal = new Decimal(e.left).mul(new Decimal(e.price));
const totalValue = secondAssetUsdPrice
? totalDecimal.mul(secondAssetUsdPrice).toFixed(2)
: undefined;
const [showTooltip, setShowTooltip] = useState(false);
async function cancelClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
event.preventDefault();
if (cancellingState) return;
try {
setCancellingState(true);
const result = await cancelOrder(e.id);
if (!result.success) {
setAlertState('error');
setAlertSubtitle('Error while cancelling order');
setTimeout(() => {
setAlertState(null);
setAlertSubtitle('');
}, 3000);
return;
}
await updateOrders();
await updateUserOrders();
await fetchUser();
} catch (error) {
console.log(error);
} finally {
setCancellingState(false);
}
}
return (
<tr key={nanoid(16)}>
<td>
<p className={styles.alias}>
<span
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
className={styles.alias__text}
>
@
{cutAddress(
state.wallet?.connected && state.wallet?.alias
? state.wallet.alias
: 'no alias',
12,
)}
</span>
<MatrixConnectionBadge
userAdress={state?.wallet?.address}
userAlias={state.wallet?.alias}
matrixAddresses={matrixAddresses}
/>
{e.isInstant && (
<div style={{ marginLeft: 2 }}>
<BadgeStatus type="instant" icon />
</div>
)}
</p>
{(state.wallet?.connected && state.wallet?.alias ? state.wallet?.alias : '')
?.length > 12 && (
<Tooltip
className={styles.tooltip}
arrowClass={styles.tooltip__arrow}
shown={showTooltip}
>
<p className={styles.tooltip__text}>
{state.wallet?.connected && state.wallet?.alias}
</p>
</Tooltip>
)}
</td>
<OrderRowTooltipCell style={{ color: e.type === 'buy' ? '#16D1D6' : '#FF6767' }}>
{notationToString(e.price)}
</OrderRowTooltipCell>
<OrderRowTooltipCell>{notationToString(e.amount)}</OrderRowTooltipCell>
<OrderRowTooltipCell
noTooltip
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
>
{notationToString(totalDecimal.toString())}{' '}
<span>~ ${totalValue && formatDollarValue(totalValue)}</span>
</OrderRowTooltipCell>
<td>
<p style={{ fontWeight: 700, color: '#1F8FEB' }}>
{applyTips?.filter((tip) => tip.connected_order_id === e.id)?.length || 0}
</p>
</td>
<td>
<Link href="/" onClick={cancelClick}>
{cancellingState ? 'Process' : 'Cancel'}
</Link>
</td>
</tr>
);
}
export default MyOrdersRow;

View file

@ -0,0 +1,13 @@
import ApplyTip from '@/interfaces/common/ApplyTip';
import MatrixAddress from '@/interfaces/common/MatrixAddress';
import OrderRow from '@/interfaces/common/OrderRow';
export interface MyOrdersRowProps {
orderData: OrderRow;
secondAssetUsdPrice: number | undefined;
updateOrders: () => Promise<void>;
updateUserOrders: () => Promise<void>;
fetchUser: () => Promise<boolean>;
matrixAddresses: MatrixAddress[];
applyTips: ApplyTip[];
}

View file

@ -0,0 +1,133 @@
import { classes } from '@/utils/utils';
import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader';
import useUpdateUser from '@/hook/useUpdateUser';
import EmptyMessage from '@/components/UI/EmptyMessage';
import styles from './styles.module.scss';
import { UserOrdersProps } from './types';
import MyOrdersRow from './components/MyOrdersRow';
import MyOrdersApplyRow from './components/MyOrdersApplyRow';
const UserOrders = ({
userOrders,
applyTips,
myOrdersLoading,
loggedIn,
ordersType,
setOrdersType,
handleCancelAllOrders,
orderListRef,
matrixAddresses,
secondAssetUsdPrice,
updateOrders,
updateUserOrders,
fetchTrades,
pairData,
}: UserOrdersProps) => {
const fetchUser = useUpdateUser();
const firstCurrencyName = pairData?.first_currency?.name || '';
const secondCurrencyName = pairData?.second_currency?.name || '';
return (
<div ref={orderListRef} className={styles.userOrders}>
<div className={styles.userOrders__header}>
<div className={styles.userOrders__header_nav}>
<button
onClick={() => setOrdersType('opened')}
className={classes(
styles.navItem,
ordersType === 'opened' && styles.active,
)}
>
Opened orders {applyTips?.length ? <span>{applyTips?.length}</span> : ''}
</button>
<button
onClick={() => setOrdersType('history')}
className={classes(
styles.navItem,
ordersType === 'history' && styles.active,
)}
>
Orders History
</button>
</div>
<div className={styles.trading__user_cancelOrder}>
<button
className={styles.userOrders__header_btn}
onClick={handleCancelAllOrders}
>
Cancel all orders
</button>
</div>
</div>
<div>
<table>
<thead>
<tr>
<th>Alias</th>
<th>Price ({secondCurrencyName})</th>
<th>Amount ({firstCurrencyName})</th>
<th>Total ({secondCurrencyName})</th>
<th>Offers</th>
<th></th>
</tr>
</thead>
</table>
{!myOrdersLoading && loggedIn && !!userOrders.length && (
<div className={`${styles.userOrders__body} orders-scroll`}>
<table>
<tbody className={styles.incoming}>
{userOrders.map((e) => (
<MyOrdersRow
key={e.id}
orderData={e}
applyTips={applyTips}
fetchUser={fetchUser}
matrixAddresses={matrixAddresses}
secondAssetUsdPrice={secondAssetUsdPrice}
updateOrders={updateOrders}
updateUserOrders={updateUserOrders}
/>
))}
</tbody>
</table>
{!!applyTips.length && (
<table className={styles.apply}>
<tbody>
{applyTips.map((e) => (
<MyOrdersApplyRow
key={e.id}
pairData={pairData}
orderData={e}
userOrders={userOrders}
fetchTrades={fetchTrades}
fetchUser={fetchUser}
matrixAddresses={matrixAddresses}
secondAssetUsdPrice={secondAssetUsdPrice}
updateOrders={updateOrders}
updateUserOrders={updateUserOrders}
/>
))}
</tbody>
</table>
)}
</div>
)}
{myOrdersLoading && loggedIn && <ContentPreloader style={{ marginTop: 40 }} />}
{!loggedIn && <EmptyMessage text="Connect wallet to see your orders" />}
{loggedIn && !userOrders.length && !myOrdersLoading && (
<EmptyMessage text="No orders" />
)}
</div>
</div>
);
};
export default UserOrders;

View file

@ -0,0 +1,227 @@
.userOrders {
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1440px) {
width: 520px;
}
&__header {
border-bottom: 1px solid var(--delimiter-color);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 10px;
padding-bottom: 0;
&_nav {
display: flex;
align-items: center;
gap: 22px;
.navItem {
padding-bottom: 7px;
position: relative;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
font-size: 16px;
border-bottom: 2px solid transparent;
font-weight: 600;
background-color: transparent;
cursor: pointer;
span {
top: -10px;
right: -15px;
position: absolute;
display: grid;
place-content: center;
color: #fff;
background-color: #ff6767;
width: 15px;
height: 15px;
font-size: 10px;
font-weight: 700;
border-radius: 50%;
}
&.active {
border-color: #1f8feb;
}
&:hover {
color: #1f8feb;
}
}
}
&_btn {
position: absolute;
right: 5px;
top: 5px;
z-index: 10;
cursor: pointer;
background-color: #1f8feb33;
color: #1f8feb;
font-size: 14px;
font-weight: 500;
padding: 6px 10px;
border-radius: 8px;
&:hover {
background-color: #1f8feb43;
}
}
}
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
padding-bottom: 5px;
margin-top: 10px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
min-width: 100px;
font-size: 11px;
font-weight: 700;
text-align: left;
color: var(--table-th-color);
&:last-child {
text-align: right;
min-width: 50px;
}
}
}
}
tbody {
a {
display: block;
text-align: right;
font-size: 12px;
font-weight: 400;
}
}
}
&__body {
padding: 10px;
padding-bottom: 20px;
height: 300px;
overflow: auto;
table {
width: 100%;
&.apply {
border-top: 1px solid #1f8feb40;
border-bottom: 1px solid #1f8feb40;
background-color: var(--blur-color) !important;
tr {
&:not(:last-child) {
border-bottom: 1px solid var(--delimiter-color);
}
}
}
tbody {
display: flex;
flex-direction: column;
&.incoming {
tr {
&:nth-child(even) {
background-color: var(--table-even-bg);
}
}
}
tr {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
td {
position: relative;
min-width: 100px;
&:last-child {
min-width: 50px;
}
@media screen and (max-width: 1440px) {
min-width: 0;
&:last-child {
min-width: 0;
}
}
> p {
width: 100%;
font-size: 12px;
font-weight: 400;
> span {
line-height: 1;
color: var(--font-dimmed-color);
font-size: 11px;
}
}
}
}
}
}
}
}
.alias {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
&__text {
color: var(--font-main-color) !important;
}
path {
fill: none;
}
}
.tooltip {
position: absolute;
top: 30px;
left: 5%;
background-color: var(--trade-table-tooltip);
z-index: 9999;
&__text {
font-size: 12px !important;
}
&__arrow {
border-radius: 2px;
left: 30% !important;
background-color: var(--trade-table-tooltip) !important;
}
}

View file

@ -0,0 +1,22 @@
import ApplyTip from '@/interfaces/common/ApplyTip';
import MatrixAddress from '@/interfaces/common/MatrixAddress';
import OrderRow from '@/interfaces/common/OrderRow';
import PairData from '@/interfaces/common/PairData';
import { Dispatch, ForwardedRef, SetStateAction } from 'react';
export interface UserOrdersProps {
orderListRef: ForwardedRef<HTMLDivElement>;
userOrders: OrderRow[];
applyTips: ApplyTip[];
myOrdersLoading: boolean;
loggedIn: boolean;
ordersType: 'opened' | 'history';
setOrdersType: Dispatch<SetStateAction<'opened' | 'history'>>;
handleCancelAllOrders: () => void;
secondAssetUsdPrice: number | undefined;
updateOrders: () => Promise<void>;
updateUserOrders: () => Promise<void>;
fetchTrades: () => Promise<void>;
matrixAddresses: MatrixAddress[];
pairData: PairData | null;
}

View file

@ -1,44 +0,0 @@
import { findPairID } from '@/utils/methods';
import { GetServerSideProps } from 'next';
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,
context.req.headers.host as string,
);
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.',
},
};
}
};
export default getServerSideProps;

View file

@ -0,0 +1,89 @@
import { Dispatch, SetStateAction } from 'react';
import Decimal from 'decimal.js';
import { isPositiveFloatStr } from '@/utils/utils';
import { validateTokensInput } from 'shared/utils';
interface HandleInputChangeParams {
inputValue: string;
priceOrAmount: 'price' | 'amount';
otherValue: string;
thisDP: number;
totalDP: number;
setThisState: Dispatch<SetStateAction<string>>;
setTotalState: Dispatch<SetStateAction<string>>;
setThisValid: Dispatch<SetStateAction<boolean>>;
setTotalValid: Dispatch<SetStateAction<boolean>>;
balance?: string | undefined;
setRangeInputValue?: Dispatch<SetStateAction<string>>;
}
export function handleInputChange({
inputValue,
priceOrAmount,
otherValue,
thisDP,
totalDP,
setThisState,
setTotalState,
setThisValid,
setTotalValid,
balance,
setRangeInputValue,
}: HandleInputChangeParams) {
if (inputValue !== '' && !isPositiveFloatStr(inputValue)) return;
const digitsOnly = inputValue.replace('.', '').replace(/^0+/, '');
if (digitsOnly.length > 18) return;
let thisDecimal: Decimal;
let otherDecimal: Decimal;
try {
thisDecimal = new Decimal(inputValue || '0');
otherDecimal = new Decimal(otherValue || '0');
} catch (err) {
console.log(err);
setThisValid(false);
setTotalValid(false);
return;
}
setThisState(inputValue);
if (!inputValue) {
setTotalState('');
setTotalValid(false);
setThisValid(false);
return;
}
const isValid = validateTokensInput(inputValue, thisDP);
if (!isValid.valid) {
setTotalState('');
setTotalValid(false);
setThisValid(false);
return;
}
setThisValid(true);
if (!thisDecimal.isNaN() && !otherDecimal.isNaN() && otherValue !== '') {
const total =
priceOrAmount === 'price'
? thisDecimal.mul(otherDecimal)
: otherDecimal.mul(thisDecimal);
setTotalState(total.toString());
const totalValid = validateTokensInput(total.toFixed(totalDP), totalDP);
setTotalValid(totalValid.valid);
if (priceOrAmount === 'amount' && balance && setRangeInputValue) {
const percent = thisDecimal.div(balance).mul(100);
setRangeInputValue(percent.toFixed());
}
} else {
setTotalState('');
setTotalValid(false);
}
}

View file

@ -0,0 +1,96 @@
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { notationToString } from '@/utils/utils';
import Decimal from 'decimal.js';
import React from 'react';
import PairDataType from '@/interfaces/common/PairData';
import OrderFormOutput from '@/interfaces/common/orderFormOutput';
import { handleInputChange } from './handleInputChange';
interface takeOrderClickParams {
event:
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
| React.MouseEvent<HTMLTableRowElement, MouseEvent>;
PageOrderData: PageOrderData;
pairData: PairDataType | null;
buyForm: OrderFormOutput;
sellForm: OrderFormOutput;
balance: string | undefined;
scrollToOrderForm: () => void;
}
function takeOrderClick({
event,
PageOrderData,
pairData,
buyForm,
sellForm,
balance,
scrollToOrderForm,
}: takeOrderClickParams) {
event.preventDefault();
const e = PageOrderData;
const priceStr = notationToString(new Decimal(e.price).toString()) || '';
const amountStr = notationToString(new Decimal(e.amount).toString()) || '';
const secondCurrencyDP = pairData?.second_currency?.asset_info?.decimal_point || 12;
const firstCurrencyDP = pairData?.first_currency?.asset_info?.decimal_point || 12;
if (e.type === 'sell') {
handleInputChange({
inputValue: priceStr,
priceOrAmount: 'price',
otherValue: amountStr,
thisDP: secondCurrencyDP,
totalDP: secondCurrencyDP,
setThisState: buyForm.setPrice,
setTotalState: buyForm.setTotal,
setThisValid: buyForm.setPriceValid,
setTotalValid: buyForm.setTotalValid,
});
handleInputChange({
inputValue: amountStr,
priceOrAmount: 'amount',
otherValue: priceStr,
thisDP: firstCurrencyDP,
totalDP: secondCurrencyDP,
setThisState: buyForm.setAmount,
setTotalState: buyForm.setTotal,
setThisValid: buyForm.setAmountValid,
setTotalValid: buyForm.setTotalValid,
balance,
setRangeInputValue: buyForm.setRangeInputValue,
});
} else {
handleInputChange({
inputValue: priceStr,
priceOrAmount: 'price',
otherValue: amountStr,
thisDP: secondCurrencyDP,
totalDP: secondCurrencyDP,
setThisState: sellForm.setPrice,
setTotalState: sellForm.setTotal,
setThisValid: sellForm.setPriceValid,
setTotalValid: sellForm.setTotalValid,
});
handleInputChange({
inputValue: amountStr,
priceOrAmount: 'amount',
otherValue: priceStr,
thisDP: firstCurrencyDP,
totalDP: secondCurrencyDP,
setThisState: sellForm.setAmount,
setTotalState: sellForm.setTotal,
setThisValid: sellForm.setAmountValid,
setTotalValid: sellForm.setTotalValid,
balance,
setRangeInputValue: sellForm.setRangeInputValue,
});
}
scrollToOrderForm();
}
export default takeOrderClick;

View file

@ -0,0 +1,47 @@
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { Trade } from '@/interfaces/responses/trades/GetTradeRes';
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
import { Store } from '@/store/store-reducer';
import { useContext } from 'react';
interface useFilteredDataParams {
trades: Trade[];
ordersHistory: PageOrderData[];
ordersBuySell: SelectValue;
tradesType: 'all' | 'my';
}
const useFilteredData = ({
ordersHistory,
trades,
ordersBuySell,
tradesType,
}: useFilteredDataParams) => {
const { state } = useContext(Store);
const filteredTrades =
tradesType === 'my'
? trades.filter(
(trade) =>
trade.buyer.address === state.wallet?.address ||
trade.seller.address === state.wallet?.address,
)
: trades;
const filteredOrdersHistory = ordersHistory
?.filter((e) => (ordersBuySell.code === 'all' ? e : e.type === ordersBuySell.code))
?.filter((e) => e.user.address !== state.wallet?.address)
?.sort((a, b) => {
if (ordersBuySell.code === 'buy') {
return parseFloat(b.price.toString()) - parseFloat(a.price.toString());
}
return parseFloat(a.price.toString()) - parseFloat(b.price.toString());
});
return {
filteredOrdersHistory,
filteredTrades,
};
};
export default useFilteredData;

View file

@ -0,0 +1,25 @@
import MatrixAddress from '@/interfaces/common/MatrixAddress';
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { getMatrixAddresses } from '@/utils/methods';
import { useEffect, useState } from 'react';
const useMatrixAddresses = (ordersHistory: PageOrderData[]) => {
const [matrixAddresses, setMatrixAddresses] = useState<MatrixAddress[]>([]);
useEffect(() => {
const fetchConnections = async () => {
const filteredAddresses = ordersHistory?.map((e) => e?.user?.address);
if (!filteredAddresses.length) return;
const data = await getMatrixAddresses(filteredAddresses);
setMatrixAddresses(data.addresses);
};
fetchConnections();
}, [ordersHistory]);
return matrixAddresses;
};
export default useMatrixAddresses;

View file

@ -0,0 +1,104 @@
import { useState, useEffect } from 'react';
import Decimal from 'decimal.js';
import PairData from '@/interfaces/common/PairData';
import OrderFormOutput from '@/interfaces/common/orderFormOutput';
import { handleInputChange } from '../helpers/handleInputChange';
interface UseOrderFormParams {
type: 'buy' | 'sell';
pairData: PairData | null;
balance: string | undefined;
assetsRates: Map<string, number>;
}
export function useOrderForm({
type,
pairData,
balance,
assetsRates,
}: UseOrderFormParams): OrderFormOutput {
const [price, setPrice] = useState('');
const [amount, setAmount] = useState('');
const [total, setTotal] = useState('');
const [priceValid, setPriceValid] = useState(false);
const [amountValid, setAmountValid] = useState(false);
const [totalValid, setTotalValid] = useState(false);
const [totalUsd, setTotalUsd] = useState<string | undefined>(undefined);
const [rangeInputValue, setRangeInputValue] = useState('50');
const priceDP = pairData?.second_currency?.asset_info?.decimal_point || 12;
const amountDP = pairData?.first_currency?.asset_info?.decimal_point || 12;
useEffect(() => {
try {
const totalDecimal = new Decimal(total);
const zanoPrice = assetsRates.get(pairData?.second_currency?.asset_id || '');
setTotalUsd(zanoPrice ? totalDecimal.mul(zanoPrice).toFixed(2) : undefined);
} catch (err) {
setTotalUsd(undefined);
}
}, [total, assetsRates, pairData?.second_currency?.asset_id]);
function onPriceChange(inputValue: string) {
handleInputChange({
inputValue,
priceOrAmount: 'price',
otherValue: amount,
thisDP: priceDP,
totalDP: priceDP,
setThisState: setPrice,
setTotalState: setTotal,
setThisValid: setPriceValid,
setTotalValid,
});
}
function onAmountChange(inputValue: string) {
handleInputChange({
inputValue,
priceOrAmount: 'amount',
otherValue: price,
thisDP: amountDP,
totalDP: priceDP,
setThisState: setAmount,
setTotalState: setTotal,
setThisValid: setAmountValid,
setTotalValid,
balance,
setRangeInputValue,
});
}
function resetForm() {
setPrice('');
setAmount('');
setTotal('');
setPriceValid(false);
setAmountValid(false);
setTotalValid(false);
setRangeInputValue('50');
}
return {
price,
amount,
total,
priceValid,
amountValid,
totalValid,
totalUsd,
rangeInputValue,
setRangeInputValue,
onPriceChange,
onAmountChange,
resetForm,
setTotal,
setPrice,
setAmount,
setPriceValid,
setAmountValid,
setTotalValid,
};
}

View file

@ -0,0 +1,85 @@
import ApplyTip from '@/interfaces/common/ApplyTip';
import OrderRow from '@/interfaces/common/OrderRow';
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { PairStats } from '@/interfaces/responses/orders/GetPairStatsRes';
import { getUserOrdersPage } from '@/utils/methods';
import socket from '@/utils/socket';
import { useRouter } from 'next/router';
import { Dispatch, SetStateAction, useEffect } from 'react';
interface useSocketListenersParams {
setUserOrders: Dispatch<SetStateAction<OrderRow[]>>;
setApplyTips: Dispatch<SetStateAction<ApplyTip[]>>;
setPairStats: Dispatch<SetStateAction<PairStats | null>>;
setOrdersHistory: Dispatch<SetStateAction<PageOrderData[]>>;
ordersHistory: PageOrderData[];
updateOrders: () => Promise<void>;
}
export const useSocketListeners = ({
setUserOrders,
setApplyTips,
setPairStats,
setOrdersHistory,
ordersHistory,
updateOrders,
}: useSocketListenersParams) => {
const router = useRouter();
const pairId = typeof router.query.id === 'string' ? router.query.id : '';
async function socketUpdateOrders() {
const result = await getUserOrdersPage(pairId);
if (result.success) {
setUserOrders(result?.data?.orders || []);
setApplyTips(result?.data?.applyTips || []);
}
}
useEffect(() => {
socket.emit('in-trading', { id: router.query.id });
return () => {
socket.emit('out-trading', { id: router.query.id });
};
}, []);
useEffect(() => {
socket.on('new-order', async (data) => {
setOrdersHistory([data.orderData, ...ordersHistory]);
await socketUpdateOrders();
});
socket.on('delete-order', async () => {
await updateOrders();
await socketUpdateOrders();
});
return () => {
socket.off('new-order');
socket.off('delete-order');
};
}, [ordersHistory]);
useEffect(() => {
function onUpdateStats({ pairStats }: { pairStats: PairStats }) {
setPairStats(pairStats);
}
socket.on('update-pair-stats', onUpdateStats);
return () => {
socket.off('update-pair-stats', onUpdateStats);
};
}, []);
useEffect(() => {
socket.on('update-orders', async () => {
await socketUpdateOrders();
});
return () => {
socket.off('update-orders');
};
}, []);
};

View file

@ -0,0 +1,70 @@
import PairData from '@/interfaces/common/PairData';
import { Store } from '@/store/store-reducer';
import { useContext } from 'react';
import Decimal from 'decimal.js';
import { PairStats } from '@/interfaces/responses/orders/GetPairStatsRes';
import { useOrderForm } from './useOrdereForm';
interface useTradeInitParams {
pairData: PairData | null;
pairStats: PairStats | null;
}
const useTradeInit = ({ pairData, pairStats }: useTradeInitParams) => {
const { state } = useContext(Store);
const currencyNames = {
firstCurrencyName: pairData?.first_currency?.name || '',
secondCurrencyName: pairData?.second_currency?.name || '',
};
const loggedIn = !!state.wallet?.connected;
const assets = state.wallet?.connected ? state.wallet?.assets || [] : [];
const balance = assets.find((e) => e.ticker === currencyNames.firstCurrencyName)?.balance;
const firstAssetId = pairData ? pairData.first_currency?.asset_id : undefined;
const secondAssetId = pairData ? pairData.second_currency?.asset_id : undefined;
const firstAssetLink = firstAssetId
? `https://explorer.zano.org/assets?asset_id=${encodeURIComponent(firstAssetId)}`
: undefined;
const secondAssetLink = secondAssetId
? `https://explorer.zano.org/assets?asset_id=${encodeURIComponent(secondAssetId)}`
: undefined;
const secondAssetUsdPrice = state.assetsRates.get(secondAssetId || '');
const pairRateUsd =
pairStats?.rate !== undefined && secondAssetUsdPrice !== undefined
? new Decimal(pairStats.rate)
.mul(secondAssetUsdPrice)
.toFixed(pairStats.rate < 0.1 ? 6 : 2)
: undefined;
const buyForm = useOrderForm({
type: 'buy',
pairData,
balance,
assetsRates: state.assetsRates,
});
const sellForm = useOrderForm({
type: 'sell',
pairData,
balance,
assetsRates: state.assetsRates,
});
return {
currencyNames,
firstAssetLink,
secondAssetLink,
secondAssetUsdPrice,
loggedIn,
balance,
buyForm,
sellForm,
pairRateUsd,
};
};
export default useTradeInit;

View file

@ -0,0 +1,144 @@
import {
getCandles,
getOrdersPage,
getPair,
getPairStats,
getUserOrdersPage,
getTrades,
} from '@/utils/methods';
import useUpdateUser from '@/hook/useUpdateUser';
import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react';
import CandleRow from '@/interfaces/common/CandleRow';
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
import { Trade } from '@/interfaces/responses/trades/GetTradeRes';
import PairData from '@/interfaces/common/PairData';
import { PairStats } from '@/interfaces/responses/orders/GetPairStatsRes';
import OrderRow from '@/interfaces/common/OrderRow';
import ApplyTip from '@/interfaces/common/ApplyTip';
import { useRouter } from 'next/router';
import PeriodState from '@/interfaces/states/pages/dex/trading/InputPanelItem/PeriodState';
import { Store } from '@/store/store-reducer';
interface UseTradingDataParams {
periodsState: PeriodState;
setCandles: Dispatch<SetStateAction<CandleRow[]>>;
setOrdersHistory: Dispatch<SetStateAction<PageOrderData[]>>;
setTrades: Dispatch<SetStateAction<Trade[]>>;
setPairData: Dispatch<SetStateAction<PairData | null>>;
setPairStats: Dispatch<SetStateAction<PairStats | null>>;
setUserOrders: Dispatch<SetStateAction<OrderRow[]>>;
setApplyTips: Dispatch<SetStateAction<ApplyTip[]>>;
setMyOrdersLoading: Dispatch<SetStateAction<boolean>>;
}
export function useTradingData({
periodsState,
setCandles,
setOrdersHistory,
setTrades,
setPairData,
setPairStats,
setUserOrders,
setApplyTips,
setMyOrdersLoading,
}: UseTradingDataParams) {
const { state } = useContext(Store);
const fetchUser = useUpdateUser();
const router = useRouter();
const [candlesLoaded, setCandlesLoaded] = useState(false);
const [ordersLoading, setOrdersLoading] = useState(true);
const [tradesLoading, setTradesLoading] = useState(true);
const pairId = typeof router.query.id === 'string' ? router.query.id : '';
const loggedIn = !!state.wallet?.connected;
async function fetchCandles() {
setCandlesLoaded(false);
setCandles([]);
const result = await getCandles(pairId, periodsState.code);
if (result.success) {
setCandles(result.data);
} else {
setCandles([]);
}
setCandlesLoaded(true);
}
async function updateOrders() {
setOrdersLoading(true);
const result = await getOrdersPage(pairId);
if (!result.success) return;
setOrdersHistory(result?.data || []);
setOrdersLoading(false);
}
async function updateUserOrders() {
setMyOrdersLoading(true);
const result = await getUserOrdersPage(pairId);
await fetchUser();
if (!result.success) return;
setUserOrders(result?.data?.orders || []);
setApplyTips(result?.data?.applyTips || []);
setMyOrdersLoading(false);
}
async function fetchTrades() {
setTradesLoading(true);
const result = await getTrades(pairId);
if (result.success) {
setTrades(result.data);
}
setTradesLoading(false);
}
async function fetchPairStats() {
const result = await getPairStats(pairId);
if (!result.success) return;
setPairStats(result.data);
}
async function getPairData() {
const result = await getPair(pairId);
if (!result.success) {
router.push('/404');
return;
}
setPairData(result.data);
}
useEffect(() => {
fetchPairStats();
getPairData();
updateOrders();
}, []);
useEffect(() => {
fetchCandles();
}, [periodsState]);
useEffect(() => {
(async () => {
await fetchTrades();
})();
}, [pairId]);
useEffect(() => {
if (!loggedIn) return;
setUserOrders([]);
updateUserOrders();
}, [state.wallet?.connected && state.wallet?.address]);
return {
fetchCandles,
updateOrders,
updateUserOrders,
fetchTrades,
fetchPairStats,
getPairData,
candlesLoaded,
ordersLoading,
tradesLoading,
};
}

View file

@ -10,6 +10,8 @@ const initialState: ContextState = {
offers: 0,
},
closed_notifications: [],
alertState: null,
alertSubtitle: '',
};
const reducer = (state: ContextState, action: ContextAction): ContextState => {
@ -31,6 +33,12 @@ const reducer = (state: ContextState, action: ContextAction): ContextState => {
case 'CLOSED_NOTIFICATIONS_UPDATED': {
return { ...state, closed_notifications: action.payload };
}
case 'ALERT_STATE_UPDATED': {
return { ...state, alertState: action.payload };
}
case 'ALERT_SUBTITLE_UPDATED': {
return { ...state, alertSubtitle: action.payload };
}
default:
return { ...state };
}
@ -40,6 +48,7 @@ export const Store = createContext<ContextValue>({
state: initialState,
dispatch: () => undefined,
});
export const StoreProvider = (props: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <Store.Provider value={{ state, dispatch }}>{props.children}</Store.Provider>;

View file

@ -1,238 +1,18 @@
.main {
padding: 0 60px;
.trading {
padding-inline: 60px;
padding-top: 20px;
display: flex;
flex-direction: column;
@media screen and (max-width: 1060px) {
padding-right: 20px;
padding-left: 20px;
@media screen and (max-width: 1600px) {
padding-inline: 40px;
}
.orders__preloader {
margin-top: 40px;
@media screen and (max-width: 1200px) {
padding-inline: 20px;
}
table {
.alias {
display: flex;
align-items: center;
gap: 4px;
p {
font-size: 14px;
}
path {
fill: none;
}
&__tooltip {
position: absolute;
top: 30px;
left: 5%;
background-color: var(--trade-table-tooltip);
font-size: 12px;
&_arrow {
border-radius: 2px;
left: 50%;
background-color: var(--trade-table-tooltip);
}
}
}
}
.trading__title__wrapper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 25px;
position: relative;
border: none;
.currency__stats__wrapper {
display: flex;
flex-wrap: nowrap;
gap: 20px;
&_assets {
display: flex;
flex-direction: column;
gap: 7px;
.asset {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
p {
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
font-weight: 400;
}
a {
font-size: 14px;
font-weight: 400;
}
}
}
> :nth-child(2),
:nth-child(4),
:nth-child(3) {
padding-left: 20px;
border-left: 1px solid var(--delimiter-color);
}
.trading__stat__item {
display: flex;
flex-direction: column;
gap: 6px;
&_nav {
display: flex;
align-items: center;
gap: 5px;
p {
color: var(--footer-selected-link);
white-space: nowrap;
font-size: 14px;
font-weight: 700;
}
}
&_content {
display: flex;
align-items: center;
gap: 5px;
.val {
white-space: nowrap;
font-size: 14px;
font-weight: 400;
}
.coefficient {
white-space: nowrap;
font-size: 14px;
font-weight: 400;
&__green {
color: #16d1d6;
}
&__red {
color: #ff6767;
}
}
}
}
@media screen and (max-width: 600px) {
gap: 0;
flex-wrap: nowrap;
flex-direction: column;
>div {
width: 100% !important;
padding-left: 0 !important;
border-left: none !important;
padding: 20px 0 !important;
border-bottom: 1px solid var(--delimiter-color);
}
}
}
.trading__currency__wrapper {
display: flex;
flex-direction: column;
gap: 15px;
.trading__currency__wrapper_top {
display: flex;
align-items: center;
gap: 12px;
.coin__icon {
min-width: 48px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--icon-bg-color);
border-radius: 50%;
img {
width: 25px;
height: auto;
}
}
.coin__currency {
display: flex;
flex-direction: column;
justify-content: space-between;
>p:first-child {
font-size: 18px;
font-weight: 600;
span {
color: var(--footer-selected-link);
}
}
.trading__currency__rate {
display: flex;
align-items: center;
gap: 5px;
.trading__currency__rate_secondCurrency {
font-weight: 400;
font-size: 14px;
}
.trading__currency__rate_usd {
color: var(--footer-selected-link);
font-size: 12px;
font-weight: 400;
}
}
}
@media screen and (max-width: 400px) {
>div:first-child {
width: 48px;
height: 48px;
>img {
scale: 0.7;
}
}
>div:last-child {
>p:first-child {
font-size: 24px;
>span {
font-size: 24px;
}
}
}
}
}
}
}
.trading__top__wrapper {
&__top {
margin-top: 20px;
display: flex;
gap: 20px;
@ -241,412 +21,19 @@
min-height: 380px;
max-height: 500px;
.trading__orders_panel {
max-width: 415px;
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1480px) {
max-width: 340px;
}
.tooltip__arrow {
border-top: 1px solid var(--dex-tooltip-border-color);
background-color: var(--dex-tooltip-bg);
}
.tooltip {
pointer-events: none;
position: fixed;
border: 1px solid var(--dex-tooltip-border-color);
width: 140px;
padding: 10px;
transform: translateX(-50%);
background-color: var(--dex-tooltip-bg);
h6 {
color: var(--table-th-color);
margin-top: 12px;
font-size: 11px;
font-weight: 700;
&:first-child {
margin-top: 0;
}
}
p {
font-size: 12px;
font-weight: 400;
display: flex;
align-items: center;
gap: 5px;
margin-top: 6px;
}
span {
margin-top: 5px;
display: block;
color: #8d95ae;
font-size: 11px;
font-weight: 400;
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px;
padding-bottom: 10px;
border-bottom: 1px solid var(--delimiter-color);
&_title {
font-size: 18px;
font-weight: 600;
}
&_type {
display: flex;
align-items: center;
gap: 8px;
button {
cursor: pointer;
width: 20px;
height: 20px;
border-radius: 4px;
font-size: 12px;
font-weight: 700;
transition: 0.3s opacity ease;
color: #ffffff;
&.selected,
&:hover {
opacity: 80%;
}
&.all {
background: linear-gradient(to left, #ff6767 50%, #16d1d6 50%);
}
&.buy {
background-color: #16d1d6;
}
&.sell {
background-color: #ff6767;
}
}
}
}
.orders__panel_content {
display: flex;
flex-direction: column;
padding-top: 10px;
}
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
margin-bottom: 9px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
font-size: 11px;
font-weight: 700;
text-align: start;
color: var(--table-th-color);
min-width: 80px;
&:last-child {
text-align: right;
}
}
}
}
tbody {
height: 29dvh;
min-height: 265px;
max-height: 380px;
display: flex;
flex-direction: column;
overflow: auto;
padding-bottom: 20px;
padding: 10px;
tr {
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
&:nth-child(even) {
background-color: var(--table-even-bg);
}
&::after {
content: '';
pointer-events: none;
position: absolute;
z-index: 1;
right: 0;
top: 0;
width: var(--line-width, 0%);
height: 100%;
background: #16d1d61a;
}
&.sell_section {
&::after {
background: #ff67671a;
}
}
td {
position: relative;
&:last-child {
>p {
text-align: right;
}
}
>p {
min-width: 80px;
width: 100%;
font-size: 12px;
font-weight: 400;
}
}
}
}
}
.orders__message {
width: 100%;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
svg {
transform: scale(0.8);
}
&.all__orders__msg {
padding-right: 30px;
}
&.user__orders__msg {
padding-right: 18px;
}
@media screen and (max-width: 550px) {
&.all__orders__msg {
padding-right: 20px;
}
&.user__orders__msg {
padding-right: 13px;
}
}
>h6 {
color: var(--font-dimmed-color);
}
}
}
.allTrades {
max-width: 415px;
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1480px) {
max-width: 340px;
}
&__header {
border-bottom: 1px solid var(--delimiter-color);
display: flex;
align-items: center;
gap: 22px;
padding: 10px;
padding-bottom: 0;
.navItem {
padding-bottom: 7px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
font-size: 16px;
border-bottom: 2px solid transparent;
font-weight: 600;
background-color: transparent;
cursor: pointer;
&.active {
border-color: #1f8feb;
}
&:hover {
color: #1f8feb;
}
}
}
.orders__panel_content {
display: flex;
flex-direction: column;
padding-top: 10px;
}
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
margin-bottom: 9px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
min-width: 80px;
font-size: 11px;
font-weight: 700;
text-align: start;
color: var(--table-th-color);
&:last-child {
text-align: right;
}
}
}
}
tbody {
height: 29dvh;
min-height: 265px;
max-height: 380px;
display: flex;
flex-direction: column;
overflow: auto;
padding: 10px;
padding-bottom: 20px;
tr {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
&:nth-child(even) {
background-color: var(--table-even-bg);
}
td {
position: relative;
&:last-child {
>p {
text-align: right;
}
}
>p {
min-width: 80px;
width: 100%;
font-size: 12px;
font-weight: 400;
}
}
}
}
}
.orders__message {
width: 100%;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
svg {
transform: scale(0.8);
}
&.all__orders__msg {
padding-right: 30px;
}
&.user__orders__msg {
padding-right: 18px;
}
@media screen and (max-width: 550px) {
&.all__orders__msg {
padding-right: 20px;
}
&.user__orders__msg {
padding-right: 13px;
}
}
>h6 {
color: var(--font-dimmed-color);
}
}
}
.trading__chart__wrapper {
&_chart {
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.trading__chart__preloader {
height: 100%;
}
.trading__chart__settings {
.settings {
display: flex;
align-items: center;
justify-content: space-between;
.trading__chart__dropdown {
width: 254px;
&__dropdown {
width: 250px;
height: 48px;
@media screen and (max-width: 1360px) {
@ -657,7 +44,7 @@
}
}
.trading__info {
&__info {
display: flex;
gap: 20px;
margin-bottom: 40px;
@ -668,327 +55,5 @@
gap: 20px;
width: 100%;
}
&_createOrder {
width: 100%;
padding: 15px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
}
.trading__user__orders {
width: 100%;
padding: 5px;
background: var(--window-bg-color);
border: 1px solid var(--delimiter-color);
border-radius: 10px;
@media screen and (max-width: 1440px) {
width: 520px;
}
&__header {
border-bottom: 1px solid var(--delimiter-color);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
padding: 10px;
padding-bottom: 0;
&_nav {
display: flex;
align-items: center;
gap: 22px;
.navItem {
padding-bottom: 7px;
position: relative;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
font-size: 16px;
border-bottom: 2px solid transparent;
font-weight: 600;
background-color: transparent;
cursor: pointer;
span {
top: -10px;
right: -15px;
position: absolute;
display: grid;
place-content: center;
color: #fff;
background-color: #ff6767;
width: 15px;
height: 15px;
font-size: 10px;
font-weight: 700;
border-radius: 50%;
}
&.active {
border-color: #1f8feb;
}
&:hover {
color: #1f8feb;
}
}
}
&_btn {
position: absolute;
right: 5px;
top: 5px;
z-index: 10;
cursor: pointer;
background-color: #1f8feb33;
color: #1f8feb;
font-size: 14px;
font-weight: 500;
padding: 6px 10px;
border-radius: 8px;
&:hover {
background-color: #1f8feb43;
}
}
}
th {
br {
display: none;
}
}
table {
width: 100%;
thead {
display: flex;
width: 100%;
padding-inline: 10px;
padding-bottom: 5px;
margin-top: 10px;
tr {
width: 100%;
display: flex;
justify-content: space-between;
th {
min-width: 100px;
font-size: 11px;
font-weight: 700;
text-align: start;
color: var(--table-th-color);
&:last-child {
text-align: right;
min-width: 50px;
}
}
}
}
tbody {
a {
display: block;
text-align: right;
font-size: 12px;
font-weight: 400;
}
}
}
.trading__right__tables {
padding: 10px;
padding-bottom: 20px;
height: 300px;
overflow: auto;
table {
width: 100%;
&.trading__apply__table {
border-top: 1px solid #1f8feb40;
border-bottom: 1px solid #1f8feb40;
background-color: var(--blur-color) !important;
tr {
&:not(:last-child) {
border-bottom: 1px solid var(--delimiter-color);
}
}
}
tbody {
display: flex;
flex-direction: column;
&.stats__table__incoming {
tr {
&:nth-child(even) {
background-color: var(--table-even-bg);
}
}
}
tr {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
td {
position: relative;
min-width: 100px;
&:last-child {
min-width: 50px;
}
@media screen and (max-width: 1440px) {
min-width: 0;
&:last-child {
min-width: 0;
}
}
>p {
width: 100%;
font-size: 12px;
font-weight: 400;
>span {
line-height: 1;
color: var(--font-dimmed-color);
font-size: 11px;
}
}
}
}
}
}
}
}
.orders__message {
width: 100%;
margin-top: 40px !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
svg {
transform: scale(0.8);
}
&.all__orders__msg {
padding-right: 30px;
}
&.user__orders__msg {
padding-right: 18px;
}
@media screen and (max-width: 550px) {
&.all__orders__msg {
padding-right: 20px;
}
&.user__orders__msg {
padding-right: 13px;
}
}
>h6 {
color: var(--font-dimmed-color);
}
}
}
.table__tooltip {
position: absolute;
top: 30px;
left: 20%;
transform: translateX(-50%);
background-color: var(--trade-table-tooltip);
>.table__tooltip_arrow {
background-color: var(--trade-table-tooltip);
}
}
.table__tooltip_right {
position: absolute;
top: 30px;
left: 2%;
background-color: var(--trade-table-tooltip);
>.table__tooltip_arrow {
left: 40px;
background-color: var(--trade-table-tooltip);
}
}
.table__tooltip_end {
position: absolute;
top: 30px;
left: -50%;
background-color: var(--trade-table-tooltip);
>.table__tooltip_arrow {
border-radius: 2px;
left: 10%;
background-color: var(--trade-table-tooltip);
}
}
.badge {
width: fit-content;
padding: 1px 4px;
padding-right: 8px;
display: flex;
align-items: center;
gap: 2px;
border-radius: 100px;
background: radial-gradient(100% 246.57% at 0% 0%, #a366ff 0%, #601fff 100%);
&.icon {
min-width: 15px;
height: 15px;
border-radius: 50%;
justify-content: center;
padding: 0;
>img {
width: 11px;
height: 11px;
}
}
>img {
height: 13px;
width: auto;
}
>span {
font-size: 10px;
font-weight: 600;
}
&.high {
padding: 2px 4px;
background: radial-gradient(100% 188.88% at 0% 0%, #16d1d6 0%, #274cff 100%);
}
}
}
}

View file

@ -0,0 +1,53 @@
[data-theme='dark'] {
--main-bg-color: #0c0c3a;
--table-header-font-color: rgba(213, 213, 226, 1);
--table-header-bg: rgba(36, 36, 78, 1);
--table-button-bg-hover: rgba(40, 40, 83, 1);
--font-main-color: rgba(255, 255, 255, 1);
--font-dimmed-color: rgba(141, 149, 174, 1);
--delimiter-color: rgba(255, 255, 255, 0.1);
--window-bg-color: rgba(15, 32, 85, 1);
--dropdown-bg-color: rgba(17, 49, 107, 1);
--dropdown-bg-hover: rgba(29, 59, 114, 1);
--profile-widget-avatar: rgba(17, 49, 107, 1);
--window-border-color: rgba(255, 255, 255, 0.3);
--bordered-input-bg: rgba(255, 255, 255, 0.1);
--row-header-bg: #154d91;
--button-bordered-hover: rgba(255, 255, 255, 0.1);
--alert-bg: rgba(35, 52, 103, 1);
--switch-bg-color: rgba(15, 32, 85, 1);
--switch-bg-hover: rgba(39, 54, 102, 1);
--switch-disabled-bg-color: rgba(255, 255, 255, 0.1);
--dimmed-btn-bg: rgba(255, 255, 255, 0.1);
--dimmed-btn-hover: rgba(255, 255, 255, 0.3);
--font-faded-color: rgba(255, 255, 255, 0.5);
--slider-bg-color: #1d3b72;
--blur-color: rgba(255, 255, 255, 0.05);
--icon-bg-color: #0f1f54;
--swap-btn-bg: rgba(31, 143, 235, 0.1);
--table-bg-color: rgba(16, 16, 64, 1);
--advices-bg-color: rgba(255, 255, 255, 0.1);
--messenger-top-bg: #0f2055;
--messenger-bottom-bg: #273666;
--messenger-bg: #0f2055;
--messenger-border: rgba(255, 255, 255, 0.1);
--message-bg: rgba(255, 255, 255, 0.1);
--custom-message-bg: transparent;
--trade-table-tooltip: #1d3b72;
--dex-offer-notification: #0f2055;
--dex-panel-bg: #0f2055;
--dex-panel-tooltip: #273666;
--dex-buy-sell-border: #ffffff1a;
--dex-input-currency: #39497c;
--footer-selected-link: #ffffffcc;
--table-th-color: #ffffffb2;
--table-thead-bg: #24244e;
--table-tbody-bg: #101040;
--admin-table-border-color: #596f98;
--alert-btn-bg: rgba(31, 143, 235, 0.2);
--alert-btn-hover: rgba(31, 143, 235, 0.3);
--table-even-bg: #0c1d4f;
--table-tr-hover-color: #172a66;
--dex-tooltip-bg: #11316b;
--dex-tooltip-border-color: #1f8feb26;
}

View file

@ -51,57 +51,3 @@
--dex-tooltip-bg: #eff8ff;
--dex-tooltip-border-color: #1f8feb33;
}
[data-theme='dark'] {
--main-bg-color: #0c0c3a;
--table-header-font-color: rgba(213, 213, 226, 1);
--table-header-bg: rgba(36, 36, 78, 1);
--table-button-bg-hover: rgba(40, 40, 83, 1);
--font-main-color: rgba(255, 255, 255, 1);
--font-dimmed-color: rgba(141, 149, 174, 1);
--delimiter-color: rgba(255, 255, 255, 0.1);
--window-bg-color: rgba(15, 32, 85, 1);
--dropdown-bg-color: rgba(17, 49, 107, 1);
--dropdown-bg-hover: rgba(29, 59, 114, 1);
--profile-widget-avatar: rgba(17, 49, 107, 1);
--window-border-color: rgba(255, 255, 255, 0.3);
--bordered-input-bg: rgba(255, 255, 255, 0.1);
--row-header-bg: #154d91;
--button-bordered-hover: rgba(255, 255, 255, 0.1);
--alert-bg: rgba(35, 52, 103, 1);
--switch-bg-color: rgba(15, 32, 85, 1);
--switch-bg-hover: rgba(39, 54, 102, 1);
--switch-disabled-bg-color: rgba(255, 255, 255, 0.1);
--dimmed-btn-bg: rgba(255, 255, 255, 0.1);
--dimmed-btn-hover: rgba(255, 255, 255, 0.3);
--font-faded-color: rgba(255, 255, 255, 0.5);
--slider-bg-color: #1d3b72;
--blur-color: rgba(255, 255, 255, 0.05);
--icon-bg-color: #0f1f54;
--swap-btn-bg: rgba(31, 143, 235, 0.1);
--table-bg-color: rgba(16, 16, 64, 1);
--advices-bg-color: rgba(255, 255, 255, 0.1);
--messenger-top-bg: #0f2055;
--messenger-bottom-bg: #273666;
--messenger-bg: #0f2055;
--messenger-border: rgba(255, 255, 255, 0.1);
--message-bg: rgba(255, 255, 255, 0.1);
--custom-message-bg: transparent;
--trade-table-tooltip: #1d3b72;
--dex-offer-notification: #0f2055;
--dex-panel-bg: #0f2055;
--dex-panel-tooltip: #273666;
--dex-buy-sell-border: #ffffff1a;
--dex-input-currency: #39497c;
--footer-selected-link: #ffffffcc;
--table-th-color: #ffffffb2;
--table-thead-bg: #24244e;
--table-tbody-bg: #101040;
--admin-table-border-color: #596f98;
--alert-btn-bg: rgba(31, 143, 235, 0.2);
--alert-btn-hover: rgba(31, 143, 235, 0.3);
--table-even-bg: #0c1d4f;
--table-tr-hover-color: #172a66;
--dex-tooltip-bg: #11316b;
--dex-tooltip-border-color: #1f8feb26;
}

View file

@ -335,3 +335,15 @@ export async function getTrades(pairId: string) {
})
.then((res) => res.data);
}
export async function getMatrixAddresses(addresses: string[]) {
try {
const { data } = await axios.post('https://messenger.zano.org/api/get-addresses', {
addresses,
});
return data;
} catch (error) {
console.log(error);
}
}

View file

@ -143,9 +143,9 @@ export function formatTime(ts: string | number) {
return date.toLocaleTimeString('ru-RU', { hour12: false });
}
export function classes(...items: (string | boolean | undefined)[]): string {
export function classes(...classes: (string | boolean | undefined)[]): string {
// boolean for constructions like [predicate] && [className]
return items.filter((className) => className).join(' ');
return classes.filter((className) => className).join(' ');
}
export const ZANO_ASSET_ID = 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a';