Merge branch 'staging'
This commit is contained in:
commit
52c02a14bc
128 changed files with 6291 additions and 3422 deletions
10
public/ui/premium.svg
Normal file
10
public/ui/premium.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="15" height="15" rx="7.5" fill="url(#paint0_radial_3055_10518)"/>
|
||||
<path d="M3 11.6302H12V2.63024H3V11.6302Z" fill="white"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_3055_10518" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(45) scale(21.2132 26.9658)">
|
||||
<stop stop-color="#16D1D6"/>
|
||||
<stop offset="1" stop-color="#274CFF"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
|
|
@ -1 +1 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.688 6.75h13.5l-3.376-3.375M16.313 11.25h-13.5l3.374 3.375" stroke="#1F8FEB" stroke-width="1.5" stroke-linecap="square"></path></svg>
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.688 6.75h13.5l-3.376-3.375M16.313 11.25h-13.5l3.374 3.375" stroke="#1F8FEB" stroke-width="1.5" stroke-linecap="square"></path></svg>
|
||||
|
Before Width: | Height: | Size: 239 B After Width: | Height: | Size: 216 B |
4
src/assets/images/UI/question.svg
Normal file
4
src/assets/images/UI/question.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="11.375" stroke="#1F8FEB" stroke-width="1.25"/>
|
||||
<path d="M10.9898 14.6042V14.5354C10.9974 13.805 11.074 13.2237 11.2194 12.7916C11.3648 12.3595 11.5714 12.0096 11.8393 11.7419C12.1071 11.4742 12.4286 11.2275 12.8036 11.0019C13.0293 10.8642 13.2321 10.7017 13.412 10.5143C13.5918 10.3231 13.7334 10.1033 13.8367 9.85468C13.9439 9.60612 13.9974 9.33078 13.9974 9.02868C13.9974 8.65392 13.9094 8.32887 13.7334 8.05354C13.5574 7.7782 13.3221 7.56597 13.0274 7.41683C12.7328 7.26769 12.4056 7.19312 12.0459 7.19312C11.7321 7.19312 11.4298 7.25813 11.139 7.38815C10.8482 7.51816 10.6052 7.72275 10.4101 8.00191C10.2149 8.28107 10.102 8.64627 10.0714 9.09751H8.625C8.65561 8.44742 8.82398 7.89101 9.1301 7.4283C9.44005 6.96558 9.84758 6.61185 10.3527 6.36711C10.8616 6.12237 11.426 6 12.0459 6C12.7194 6 13.3048 6.13384 13.8023 6.40153C14.3036 6.66922 14.6901 7.03633 14.9617 7.50287C15.2372 7.96941 15.375 8.50096 15.375 9.09751C15.375 9.51816 15.3099 9.89866 15.1798 10.239C15.0536 10.5793 14.8699 10.8834 14.6288 11.1511C14.3916 11.4187 14.1046 11.6558 13.7679 11.8623C13.4311 12.0727 13.1614 12.2945 12.9585 12.5277C12.7557 12.7572 12.6084 13.0306 12.5166 13.348C12.4247 13.6654 12.375 14.0612 12.3673 14.5354V14.6042H10.9898ZM11.7245 18C11.4413 18 11.1983 17.8987 10.9955 17.696C10.7927 17.4933 10.6913 17.2505 10.6913 16.9675C10.6913 16.6845 10.7927 16.4417 10.9955 16.239C11.1983 16.0363 11.4413 15.935 11.7245 15.935C12.0077 15.935 12.2506 16.0363 12.4534 16.239C12.6563 16.4417 12.7577 16.6845 12.7577 16.9675C12.7577 17.1549 12.7098 17.327 12.6142 17.4837C12.5223 17.6405 12.398 17.7667 12.2411 17.8623C12.088 17.9541 11.9158 18 11.7245 18Z" fill="#1F8FEB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
14
src/components/UI/ActionBtn/index.tsx
Normal file
14
src/components/UI/ActionBtn/index.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import { classes } from '@/utils/utils';
|
||||
import { ActionBtnProps } from './types';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const ActionBtn = ({ children, variant = 'primary', className, ...props }: ActionBtnProps) => {
|
||||
return (
|
||||
<button className={classes(styles.btn, className, styles[variant])} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionBtn;
|
||||
33
src/components/UI/ActionBtn/styles.module.scss
Normal file
33
src/components/UI/ActionBtn/styles.module.scss
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.btn {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 5px;
|
||||
background: var(--action-btn-bg);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
min-width: max-content;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--action-btn-bg);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: #1f8feb;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #16d1d6;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff6767;
|
||||
}
|
||||
}
|
||||
5
src/components/UI/ActionBtn/types.ts
Normal file
5
src/components/UI/ActionBtn/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ButtonHTMLAttributes } from 'react';
|
||||
|
||||
export interface ActionBtnProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'success' | 'danger';
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
6
src/components/UI/ContentPreloader/types.ts
Normal file
6
src/components/UI/ContentPreloader/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { CSSProperties } from 'react';
|
||||
|
||||
export interface ContentPreloaderProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
16
src/components/UI/EmptyMessage/index.tsx
Normal file
16
src/components/UI/EmptyMessage/index.tsx
Normal 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;
|
||||
17
src/components/UI/EmptyMessage/styles.module.scss
Normal file
17
src/components/UI/EmptyMessage/styles.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
6
src/components/UI/EmptyMessage/types.ts
Normal file
6
src/components/UI/EmptyMessage/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export interface EmptyMessageProps {
|
||||
text: string;
|
||||
customIcon?: ReactNode;
|
||||
}
|
||||
|
|
@ -4,8 +4,21 @@
|
|||
overflow: auto;
|
||||
padding-bottom: 3px;
|
||||
|
||||
&.sm {
|
||||
gap: 5px !important;
|
||||
|
||||
> div {
|
||||
a {
|
||||
font-size: 14px !important;
|
||||
padding: 8px !important;
|
||||
border-radius: 8px !important;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tab {
|
||||
gap: 10px;
|
||||
gap: 3px;
|
||||
|
||||
> div {
|
||||
a {
|
||||
|
|
@ -16,7 +29,8 @@
|
|||
border-radius: 10px;
|
||||
|
||||
&.selected {
|
||||
background-color: #1f8feb1a;
|
||||
background-color: var(--tab-bg-color);
|
||||
color: #fff;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Link from 'next/link';
|
|||
import { nanoid } from 'nanoid';
|
||||
import HorizontalSelectProps from '@/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps';
|
||||
import HorizontalSelectValue from '@/interfaces/common/HorizontalSelectValue';
|
||||
import { classes } from '@/utils/utils';
|
||||
import NotificationIndicator from '../NotificationIndicator/NotificationIndicator';
|
||||
import styles from './HorizontalSelect.module.scss';
|
||||
|
||||
|
|
@ -13,7 +14,12 @@ function HorizontalSelect<T extends HorizontalSelectValue>(props: HorizontalSele
|
|||
return (
|
||||
<div
|
||||
style={props.withNotifications ? { paddingTop: '20px' } : {}}
|
||||
className={`${styles.horizontal_select} ${className || ''} ${props.isTab ? styles.tab : ''}`}
|
||||
className={classes(
|
||||
styles.horizontal_select,
|
||||
className,
|
||||
props.isTab && styles.tab,
|
||||
props.isSm && styles.sm,
|
||||
)}
|
||||
>
|
||||
{props.body.map((e) => (
|
||||
<div key={nanoid(16)}>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
background: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
z-index: 5;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +18,13 @@
|
|||
top: 30px;
|
||||
transition: none;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0px 4px 14px 0px #07072b59;
|
||||
padding: 9px 12px;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.range__slider {
|
||||
|
|
@ -74,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;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function RangeInput(props: RangeInputProps) {
|
|||
className={styles.input__range__tooltip}
|
||||
shown={tooltipShown}
|
||||
>
|
||||
{realValue}%
|
||||
<p>{realValue}%</p>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
|
|
|
|||
22
src/components/UI/Tabs/index.tsx
Normal file
22
src/components/UI/Tabs/index.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import { classes } from '@/utils/utils';
|
||||
import styles from './styles.module.scss';
|
||||
import { TabsProps } from './types';
|
||||
|
||||
const Tabs = ({ type = 'tab', data, value, setValue }: TabsProps) => {
|
||||
return (
|
||||
<div className={classes(styles.tabs, styles[type])}>
|
||||
{data.map((tab) => (
|
||||
<button
|
||||
key={tab.type}
|
||||
onClick={() => setValue(tab)}
|
||||
className={classes(styles.tabs__item, value.type === tab.type && styles.active)}
|
||||
>
|
||||
{tab.title} {Number.isFinite(tab.length) && <>({tab.length})</>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
48
src/components/UI/Tabs/styles.module.scss
Normal file
48
src/components/UI/Tabs/styles.module.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.tabs {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--delimiter-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
|
||||
&.button {
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
border-bottom: none;
|
||||
|
||||
.tabs__item {
|
||||
padding: 6px 10px;
|
||||
border-radius: 25px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--action-btn-bg);
|
||||
|
||||
&.active {
|
||||
border-color: transparent;
|
||||
background-color: #1f8feb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
cursor: pointer;
|
||||
padding-bottom: 7px;
|
||||
position: relative;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
background-color: transparent;
|
||||
color: #1f8feb;
|
||||
|
||||
&.active {
|
||||
color: var(--text-color);
|
||||
border-color: #1f8feb;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #1f8feb;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/components/UI/Tabs/types.ts
Normal file
12
src/components/UI/Tabs/types.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type tabsType = {
|
||||
title: string;
|
||||
type: string;
|
||||
length?: number;
|
||||
};
|
||||
|
||||
export interface TabsProps {
|
||||
type?: 'tab' | 'button';
|
||||
value: tabsType;
|
||||
setValue: (_next: tabsType) => void;
|
||||
data: tabsType[];
|
||||
}
|
||||
|
|
@ -1,4 +1,13 @@
|
|||
.back_btn {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
&.sm {
|
||||
padding: 12px 22px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import { ReactComponent as ArrowWhiteIcon } from '@/assets/images/UI/arrow_white.svg';
|
||||
import Button from '@/components/UI/Button/Button';
|
||||
import { useRouter } from 'next/router';
|
||||
import { classes } from '@/utils/utils';
|
||||
import styles from './BackButton.module.scss';
|
||||
import { BackButtonProps } from './types';
|
||||
|
||||
function BackButton() {
|
||||
function BackButton({ className, isSm }: BackButtonProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Button className={styles.back_btn} transparent={true} onClick={router.back}>
|
||||
{/* <img src={ArrowWhiteIcon} alt="arrow"/> */}
|
||||
<Button
|
||||
className={classes(styles.back_btn, className, isSm && styles.sm)}
|
||||
transparent={true}
|
||||
onClick={router.back}
|
||||
>
|
||||
<ArrowWhiteIcon />
|
||||
Back
|
||||
<span>Back</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
4
src/components/default/BackButton/types.ts
Normal file
4
src/components/default/BackButton/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface BackButtonProps {
|
||||
className?: string;
|
||||
isSm?: boolean;
|
||||
}
|
||||
|
|
@ -29,12 +29,12 @@ const links: {
|
|||
title: 'Auction',
|
||||
type: 'auction',
|
||||
link: 'https://wrapped.zano.org/',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
title: 'Messenger',
|
||||
type: 'messenger',
|
||||
link: 'https://zano.org/',
|
||||
disabled: true,
|
||||
link: 'https://messenger.zano.org/',
|
||||
},
|
||||
{
|
||||
title: 'Wrapped Zano',
|
||||
|
|
|
|||
163
src/components/default/GenericTable/index.tsx
Normal file
163
src/components/default/GenericTable/index.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { classes } from '@/utils/utils';
|
||||
import React, { useMemo } from 'react';
|
||||
import EmptyMessage from '@/components/UI/EmptyMessage';
|
||||
import { useMediaQuery } from '@/hook/useMediaQuery';
|
||||
import { GenericTableProps } from './types';
|
||||
|
||||
export default function GenericTable<T>(props: GenericTableProps<T>) {
|
||||
const {
|
||||
className,
|
||||
tableClassName,
|
||||
theadClassName,
|
||||
tbodyClassName,
|
||||
columns,
|
||||
data,
|
||||
getRowKey,
|
||||
emptyMessage = 'No data',
|
||||
getRowProps,
|
||||
groupBy,
|
||||
renderGroupHeader,
|
||||
sortGroups,
|
||||
responsive,
|
||||
scrollRef,
|
||||
} = props;
|
||||
const isMatch = useMediaQuery(responsive?.query ?? '');
|
||||
const mediaActive = !!responsive?.query && isMatch;
|
||||
|
||||
const effectiveColumns = useMemo(() => {
|
||||
let cols = columns;
|
||||
|
||||
if (mediaActive && responsive?.hiddenKeys?.length) {
|
||||
const hide = new Set(responsive.hiddenKeys);
|
||||
cols = cols.filter((c) => !hide.has(c.key));
|
||||
}
|
||||
|
||||
if (mediaActive && responsive?.alignOverride) {
|
||||
cols = cols.map((c) => {
|
||||
const ov = responsive.alignOverride?.[c.key];
|
||||
return ov ? { ...c, align: ov } : c;
|
||||
});
|
||||
}
|
||||
|
||||
return cols;
|
||||
}, [columns, mediaActive, responsive]);
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
if (!groupBy) return [{ key: '__all__', items: data }];
|
||||
|
||||
const map = new Map<string, T[]>();
|
||||
for (const item of data) {
|
||||
const k = String(groupBy(item));
|
||||
const bucket = map.get(k) ?? [];
|
||||
bucket.push(item);
|
||||
map.set(k, bucket);
|
||||
}
|
||||
const entries = Array.from(map.entries());
|
||||
if (sortGroups) entries.sort((a, b) => sortGroups(a[0], b[0]));
|
||||
return entries.map(([key, items]) => ({ key, items }));
|
||||
}, [data, groupBy, sortGroups]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{data.length > 0 ? (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="orders-scroll"
|
||||
style={{ maxHeight: '100%', overflowY: 'auto' }}
|
||||
>
|
||||
<table
|
||||
className={tableClassName}
|
||||
style={{
|
||||
tableLayout:
|
||||
isMatch && responsive?.tableLayout
|
||||
? responsive.tableLayout
|
||||
: 'fixed',
|
||||
width: '100%',
|
||||
borderCollapse: 'separate',
|
||||
borderSpacing: 0,
|
||||
}}
|
||||
>
|
||||
<colgroup>
|
||||
{effectiveColumns.map((col) => (
|
||||
<col
|
||||
key={col.key}
|
||||
style={col.width ? { width: col.width } : undefined}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
|
||||
<thead className={theadClassName}>
|
||||
<tr>
|
||||
{effectiveColumns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className={col.className}
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: '-1px',
|
||||
zIndex: 2,
|
||||
background: 'var(--window-bg-color)',
|
||||
textAlign: col.align ?? 'left',
|
||||
whiteSpace: 'nowrap',
|
||||
overflowX: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '6px 10px',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{col.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className={tbodyClassName}>
|
||||
{grouped.map((group, gi) => (
|
||||
<React.Fragment key={group.key}>
|
||||
{renderGroupHeader && (
|
||||
<tr className="__group-header-row">
|
||||
<td colSpan={effectiveColumns.length}>
|
||||
{renderGroupHeader({
|
||||
groupKey: group.key,
|
||||
items: group.items,
|
||||
index: gi,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{group.items.map((row, i) => (
|
||||
<tr
|
||||
{...(getRowProps ? getRowProps(row, i) : {})}
|
||||
key={getRowKey(row, i)}
|
||||
>
|
||||
{effectiveColumns.map((col) => (
|
||||
<td
|
||||
key={col.key}
|
||||
className={classes(col.className)}
|
||||
style={{
|
||||
textAlign: col.align ?? 'left',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '6px 10px',
|
||||
verticalAlign: 'middle',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{col.cell(row, i)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyMessage text={emptyMessage} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/components/default/GenericTable/types.ts
Normal file
42
src/components/default/GenericTable/types.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export type Align = 'left' | 'center' | 'right';
|
||||
|
||||
export type ColumnDef<T> = {
|
||||
key: string;
|
||||
header: React.ReactNode;
|
||||
width?: string;
|
||||
align?: Align;
|
||||
className?: string;
|
||||
cell: (_row: T, _rowIndex: number) => React.ReactNode;
|
||||
};
|
||||
|
||||
export type RowProps = React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type GroupHeaderRenderArgs<T> = {
|
||||
groupKey: string;
|
||||
items: T[];
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type GenericTableProps<T> = {
|
||||
className?: string;
|
||||
tableClassName?: string;
|
||||
theadClassName?: string;
|
||||
tbodyClassName?: string;
|
||||
columns: ColumnDef<T>[];
|
||||
data: T[];
|
||||
getRowKey: (_row: T, _rowIndex: number) => React.Key;
|
||||
emptyMessage?: string;
|
||||
getRowProps?: (_row: T, _index: number) => RowProps | undefined;
|
||||
groupBy?: (_row: T) => string | number;
|
||||
renderGroupHeader?: (_args: GroupHeaderRenderArgs<T>) => React.ReactNode;
|
||||
sortGroups?: (_a: string, _b: string) => number;
|
||||
responsive?: {
|
||||
query: string;
|
||||
hiddenKeys?: string[];
|
||||
alignOverride?: Record<string, 'left' | 'center' | 'right'>;
|
||||
tableLayout?: 'auto' | 'fixed';
|
||||
};
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
|
@ -7,9 +7,53 @@
|
|||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--delimiter-color);
|
||||
background-color: var(--main-bg-color);
|
||||
z-index: 1;
|
||||
z-index: 99;
|
||||
position: relative;
|
||||
|
||||
&.lg {
|
||||
padding-inline: 60px;
|
||||
height: 65px;
|
||||
|
||||
.header__logo {
|
||||
width: 180px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.header__currency__check {
|
||||
height: 48px !important;
|
||||
gap: 25px !important;
|
||||
}
|
||||
|
||||
.header__account__wrapper {
|
||||
.header__account__info {
|
||||
p {
|
||||
&:first-child {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.header__account__btn {
|
||||
min-width: 48px !important;
|
||||
height: 48px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header__connect_btn {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
padding-inline: 40px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.header__logo {
|
||||
display: flex;
|
||||
width: 202px;
|
||||
|
|
@ -65,6 +109,7 @@
|
|||
.header__login {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: none;
|
||||
}
|
||||
|
|
@ -208,7 +253,7 @@
|
|||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: #273666;
|
||||
background-color: var(--switch-bg-color);
|
||||
border-radius: 0 0 10px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import Button from '@/components/UI/Button/Button';
|
|||
|
||||
import { useWindowWidth } from '@react-hook/window-size';
|
||||
import ConnectButton from '@/components/UI/ConnectButton/ConnectButton';
|
||||
import { notationToString, setWalletCredentials, shortenAddress } from '@/utils/utils';
|
||||
import { classes, notationToString, setWalletCredentials, shortenAddress } from '@/utils/utils';
|
||||
import useAdvancedTheme from '@/hook/useTheme';
|
||||
|
||||
import { Store } from '@/store/store-reducer';
|
||||
|
|
@ -33,7 +33,7 @@ import useUpdateUser from '@/hook/useUpdateUser';
|
|||
import NavBar from './NavBar/NavBar';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
function Header() {
|
||||
function Header({ isLg }: { isLg?: boolean }) {
|
||||
const { theme, setTheme } = useAdvancedTheme();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -347,7 +347,7 @@ function Header() {
|
|||
return (
|
||||
<>
|
||||
{menuOpened && <div className={styles.header__blur__block}></div>}
|
||||
<header className={styles.header}>
|
||||
<header className={classes(styles.header, isLg && styles.lg)}>
|
||||
<div className={styles.header__logo}>
|
||||
<Link href="/dex">
|
||||
<img src={theme === 'dark' ? logoImg : logoImgWhite} alt="Zano P2P" />
|
||||
|
|
@ -355,9 +355,11 @@ function Header() {
|
|||
</div>
|
||||
|
||||
<div className={styles.header__desktop__navigation}>
|
||||
<NavBar />
|
||||
<NavBar isLg={isLg} />
|
||||
</div>
|
||||
|
||||
{Menu()}
|
||||
|
||||
<BurgerButton className={styles.header__burger} />
|
||||
|
||||
<div className={styles.header__account__mobile} style={mobileHeaderStyle}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,25 @@
|
|||
.nav {
|
||||
&.lg {
|
||||
a {
|
||||
gap: 8px;
|
||||
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 3px;
|
||||
min-width: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import NavBarProps from '@/interfaces/props/components/default/Header/NavBar/Nav
|
|||
import NotificationIndicator from '@/components/UI/NotificationIndicator/NotificationIndicator';
|
||||
import { useContext } from 'react';
|
||||
import { Store } from '@/store/store-reducer';
|
||||
import { classes } from '@/utils/utils';
|
||||
import styles from './NavBar.module.scss';
|
||||
|
||||
function NavBar(props: NavBarProps) {
|
||||
|
|
@ -40,14 +41,18 @@ function NavBar(props: NavBarProps) {
|
|||
<Link href={href} className={linkClass}>
|
||||
<Img />
|
||||
<h6>{title}</h6>
|
||||
<NotificationIndicator count={notifications} />
|
||||
<NotificationIndicator className={styles.badge} count={notifications} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`${styles.nav} ${!props.mobile ? styles.nav__desktop : styles.nav__mobile}`}
|
||||
className={classes(
|
||||
styles.nav,
|
||||
!props.mobile ? styles.nav__desktop : styles.nav__mobile,
|
||||
props.isLg && styles.lg,
|
||||
)}
|
||||
>
|
||||
<NavItem
|
||||
title={'Exchange'}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ function PairsTable({ data }: IProps) {
|
|||
cell: ({ row }) => (
|
||||
<div className={styles.price_cell}>
|
||||
<div className={styles.text}>
|
||||
{roundTo(notationToString(row.original.price), 2)}
|
||||
{roundTo(notationToString(row.original.price), 4)}
|
||||
</div>
|
||||
<div className={styles.sub_text}>{row.original.priceUSD}</div>
|
||||
</div>
|
||||
|
|
@ -149,7 +149,7 @@ function PairsTable({ data }: IProps) {
|
|||
cell: ({ row }) => (
|
||||
<div className={styles.price_cell}>
|
||||
<div className={styles.text}>
|
||||
{roundTo(notationToString(row.original?.volume ?? 0), 2)}
|
||||
{roundTo(notationToString(row.original?.volume ?? 0), 4)}
|
||||
</div>
|
||||
<div className={styles.sub_text}>{row.original.volumeUSD}</div>
|
||||
</div>
|
||||
|
|
|
|||
98
src/components/dex/AliasCell/index.tsx
Normal file
98
src/components/dex/AliasCell/index.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import Tooltip from '@/components/UI/Tooltip/Tooltip';
|
||||
import { classes, cutAddress } from '@/utils/utils';
|
||||
import MatrixConnectionBadge from '@/components/dex/MatrixConnectionBadge';
|
||||
import BadgeStatus from '@/components/dex/BadgeStatus';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styles from './styles.module.scss';
|
||||
import { AliasCellProps } from './types';
|
||||
|
||||
export default function AliasCell({
|
||||
alias,
|
||||
address,
|
||||
matrixAddresses,
|
||||
isInstant,
|
||||
isSm,
|
||||
max = 12,
|
||||
}: AliasCellProps) {
|
||||
const display = alias ? cutAddress(alias, max) : 'no alias';
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
||||
const anchorRef = useRef<HTMLParagraphElement | null>(null);
|
||||
|
||||
const updatePosition = () => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setPos({
|
||||
top: rect.bottom + 8,
|
||||
left: rect.left + rect.width / 2,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
updatePosition();
|
||||
const onScrollOrResize = () => updatePosition();
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScrollOrResize, true);
|
||||
window.removeEventListener('resize', onScrollOrResize);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<p ref={anchorRef} className={styles.alias}>
|
||||
<span
|
||||
onMouseEnter={() => {
|
||||
setOpen(true);
|
||||
requestAnimationFrame(updatePosition);
|
||||
}}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
className={styles.alias__text}
|
||||
>
|
||||
@{display}
|
||||
</span>
|
||||
|
||||
<MatrixConnectionBadge
|
||||
userAdress={address}
|
||||
userAlias={alias}
|
||||
matrixAddresses={matrixAddresses}
|
||||
isSm={isSm}
|
||||
/>
|
||||
|
||||
{isInstant && (
|
||||
<div style={{ marginLeft: 2 }}>
|
||||
<BadgeStatus type="instant" icon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{open &&
|
||||
pos &&
|
||||
createPortal(
|
||||
<>
|
||||
{alias && alias.length > max && (
|
||||
<Tooltip
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
className={styles.tooltip}
|
||||
arrowClass={styles.tooltip__arrow}
|
||||
shown={true}
|
||||
>
|
||||
<p className={styles.tooltip__text}>{alias}</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>,
|
||||
document.body,
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
33
src/components/dex/AliasCell/styles.module.scss
Normal file
33
src/components/dex/AliasCell/styles.module.scss
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.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 !important;
|
||||
|
||||
&__text {
|
||||
font-size: 12px !important;
|
||||
color: var(--font-main-color) !important;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
border-radius: 2px;
|
||||
left: 30% !important;
|
||||
background-color: var(--trade-table-tooltip) !important;
|
||||
}
|
||||
}
|
||||
10
src/components/dex/AliasCell/types.ts
Normal file
10
src/components/dex/AliasCell/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import MatrixAddress from '@/interfaces/common/MatrixAddress';
|
||||
|
||||
export interface AliasCellProps {
|
||||
alias?: string;
|
||||
address?: string;
|
||||
matrixAddresses: MatrixAddress[];
|
||||
isInstant?: boolean;
|
||||
isSm?: boolean;
|
||||
max?: number;
|
||||
}
|
||||
26
src/components/dex/BadgeStatus/index.tsx
Normal file
26
src/components/dex/BadgeStatus/index.tsx
Normal 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;
|
||||
39
src/components/dex/BadgeStatus/styles.module.scss
Normal file
39
src/components/dex/BadgeStatus/styles.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
4
src/components/dex/BadgeStatus/types.ts
Normal file
4
src/components/dex/BadgeStatus/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface BadgeStatusProps {
|
||||
type?: 'instant' | 'high';
|
||||
icon?: boolean;
|
||||
}
|
||||
198
src/components/dex/CandleChart/index.tsx
Normal file
198
src/components/dex/CandleChart/index.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import useAdvancedTheme from '@/hook/useTheme';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import type CandleChartProps from '@/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps';
|
||||
import styles from './styles.module.scss';
|
||||
import { ResultCandle } from './types';
|
||||
import {
|
||||
buildCandles,
|
||||
d,
|
||||
diffFmt,
|
||||
fmt,
|
||||
pickWindowIndices,
|
||||
tsLabel,
|
||||
zoomStartByPeriod,
|
||||
chartColors,
|
||||
} from './utils';
|
||||
|
||||
function CandleChart(props: CandleChartProps) {
|
||||
const { theme } = useAdvancedTheme();
|
||||
const chartRef = useRef<ReactECharts>(null);
|
||||
|
||||
const [candles, setCandles] = useState<ResultCandle[]>([]);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCandles(buildCandles(props.candles));
|
||||
setIsLoaded(true);
|
||||
}, [props.candles]);
|
||||
|
||||
// shown candle
|
||||
const shownIdx = candles.length ? candles.length - 1 : null;
|
||||
const prevIdx = shownIdx && shownIdx > 0 ? shownIdx - 1 : null;
|
||||
|
||||
const shown = shownIdx !== null ? candles[shownIdx] : undefined;
|
||||
const prev = prevIdx !== null ? candles[prevIdx] : undefined;
|
||||
|
||||
const O = shown?.[3];
|
||||
const H = shown?.[1];
|
||||
const L = shown?.[2];
|
||||
const C = shown?.[4];
|
||||
const P = prev?.[4];
|
||||
const delta = diffFmt(C, P);
|
||||
|
||||
const option = useMemo(() => {
|
||||
const timestamps = candles.map((c) => c[0]);
|
||||
const { startIdx, endIdx } = pickWindowIndices(timestamps, zoomStartByPeriod(props.period));
|
||||
|
||||
return {
|
||||
grid: { top: '5%', left: '0%', right: '10%', bottom: '8%' },
|
||||
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
min: 'dataMin',
|
||||
max: 'dataMax',
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: theme === 'light' ? chartColors.light : chartColors.dark,
|
||||
},
|
||||
},
|
||||
axisLine: { onZero: false },
|
||||
axisLabel: { formatter: (v: string) => tsLabel(parseInt(v, 10)) },
|
||||
axisPointer: {
|
||||
show: true,
|
||||
type: 'line',
|
||||
label: {
|
||||
formatter: (p: { value: string }) => tsLabel(parseInt(p.value, 10)),
|
||||
backgroundColor: chartColors.default,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
scale: true,
|
||||
position: 'right',
|
||||
splitArea: { show: false },
|
||||
min: (v: { min: number; max: number }) => {
|
||||
const min = d(v.min);
|
||||
const range = d(v.max).minus(min);
|
||||
const pad = range.lte(0) ? d(v.max).mul(0.05) : range.mul(0.1);
|
||||
return min.minus(pad).toNumber();
|
||||
},
|
||||
max: (v: { min: number; max: number }) => {
|
||||
const min = d(v.min);
|
||||
const max = d(v.max);
|
||||
const range = max.minus(min);
|
||||
const pad = range.lte(0) ? max.mul(0.05) : range.mul(0.1);
|
||||
return max.plus(pad).toNumber();
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: theme === 'light' ? chartColors.light : chartColors.dark,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (val: number | string) => d(val).toDecimalPlaces(6).toString(),
|
||||
},
|
||||
axisPointer: {
|
||||
show: true,
|
||||
type: 'line',
|
||||
label: {
|
||||
show: true,
|
||||
color: '#fff',
|
||||
backgroundColor: (p: {
|
||||
seriesData?: Array<{ value?: (number | string)[] }>;
|
||||
}) => {
|
||||
const ts = p.seriesData?.[0]?.value?.[0];
|
||||
const idx = candles.findIndex((c) => c[0] === ts);
|
||||
if (idx === -1) return chartColors.default;
|
||||
const [, , , o, c] = candles[idx];
|
||||
let color = chartColors.default;
|
||||
|
||||
if (c > o) {
|
||||
color = chartColors.green;
|
||||
} else if (c < o) {
|
||||
color = chartColors.red;
|
||||
}
|
||||
|
||||
return color;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
dataZoom: [{ type: 'inside', startValue: startIdx, endValue: endIdx }],
|
||||
|
||||
series: [
|
||||
{
|
||||
name: 'Candle Chart',
|
||||
type: 'candlestick',
|
||||
data: candles,
|
||||
barWidth: '75%',
|
||||
itemStyle: {
|
||||
color: chartColors.green,
|
||||
color0: chartColors.red,
|
||||
borderColor: chartColors.green,
|
||||
borderColor0: chartColors.red,
|
||||
},
|
||||
dimensions: ['date', 'highest', 'lowest', 'open', 'close'],
|
||||
encode: { x: 'date', y: ['open', 'close', 'highest', 'lowest'] },
|
||||
large: true,
|
||||
largeThreshold: 2_000_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [candles, props.period, theme]);
|
||||
|
||||
return (
|
||||
<div className={styles.chart}>
|
||||
{/* Header */}
|
||||
<div className={styles.chart__top}>
|
||||
<div className={styles.chart__top_item}>
|
||||
<p>
|
||||
{props.currencyNames.firstCurrencyName}/
|
||||
{props.currencyNames.secondCurrencyName}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.chart__top_item}>
|
||||
<p style={{ opacity: 0.7 }}>O</p>
|
||||
<span style={{ color: delta.color }}>{fmt(O)}</span>
|
||||
</div>
|
||||
<div className={styles.chart__top_item}>
|
||||
<span style={{ opacity: 0.7 }}>H</span>
|
||||
<span style={{ color: delta.color }}>{fmt(H)}</span>
|
||||
</div>
|
||||
<div className={styles.chart__top_item}>
|
||||
<span style={{ opacity: 0.7 }}>L</span>
|
||||
<span style={{ color: delta.color }}>{fmt(L)}</span>
|
||||
</div>
|
||||
<div className={styles.chart__top_item}>
|
||||
<span style={{ opacity: 0.7 }}>C</span>
|
||||
<span style={{ color: delta.color }}>{fmt(C)}</span>
|
||||
</div>
|
||||
<div className={styles.chart__top_item}>
|
||||
<p style={{ color: delta.color }}>{delta.txt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReactECharts
|
||||
ref={chartRef}
|
||||
option={option}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
opts={{ devicePixelRatio: 2 }}
|
||||
lazyUpdate
|
||||
notMerge
|
||||
/>
|
||||
|
||||
{!candles.length && isLoaded && (
|
||||
<h1 className={styles.chart__lowVolume}>[ Low volume ]</h1>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CandleChart;
|
||||
66
src/components/dex/CandleChart/styles.module.scss
Normal file
66
src/components/dex/CandleChart/styles.module.scss
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
.chart {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
|
||||
&__top {
|
||||
padding-bottom: 10px;
|
||||
background-color: var(--main-bg-color);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
|
||||
&_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
p {
|
||||
color: var(--footer-selected-link);
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
&__lowVolume {
|
||||
font-size: 72px;
|
||||
color: var(--font-dimmed-color);
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/components/dex/CandleChart/types.ts
Normal file
1
src/components/dex/CandleChart/types.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type ResultCandle = [ts: number, high: number, low: number, open: number, close: number];
|
||||
116
src/components/dex/CandleChart/utils.ts
Normal file
116
src/components/dex/CandleChart/utils.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import CandleRow from '@/interfaces/common/CandleRow';
|
||||
import CandleChartProps from '@/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps';
|
||||
import Decimal from 'decimal.js';
|
||||
import * as echarts from 'echarts';
|
||||
import { ResultCandle } from './types';
|
||||
import testCandles from './testCandles.json';
|
||||
|
||||
export const TESTING_MODE = false;
|
||||
|
||||
export const chartColors = {
|
||||
green: '#16D1D6',
|
||||
red: '#FF6767',
|
||||
default: '#3D3D6C',
|
||||
light: '#e3e3e8',
|
||||
dark: '#1f1f4a',
|
||||
textColor: '#ffffff',
|
||||
};
|
||||
|
||||
export const d = (v: number | string) => new Decimal(v);
|
||||
export const fmt = (n?: number | string) =>
|
||||
n === undefined || n === null || Number.isNaN(Number(n))
|
||||
? '-'
|
||||
: d(n).toDecimalPlaces(6).toString();
|
||||
|
||||
export function diffFmt(curr?: number | string, prev?: number | string) {
|
||||
if (curr === undefined || prev === undefined || Number(prev) === 0) {
|
||||
return { txt: '-', color: '#9CA3AF' };
|
||||
}
|
||||
|
||||
const diff = d(curr).minus(prev);
|
||||
const pct = diff.div(prev).mul(100);
|
||||
|
||||
let sign = '';
|
||||
if (diff.greaterThan(0)) {
|
||||
sign = '+';
|
||||
} else if (diff.isZero()) {
|
||||
sign = '';
|
||||
}
|
||||
|
||||
const txt = `${sign}${diff.toDecimalPlaces(8).toString()} (${sign}${pct.toDecimalPlaces(2).toString()}%)`;
|
||||
|
||||
let color = '#9CA3AF';
|
||||
if (diff.greaterThan(0)) {
|
||||
color = chartColors.green;
|
||||
} else if (diff.lessThan(0)) {
|
||||
color = chartColors.red;
|
||||
}
|
||||
|
||||
return { txt, color };
|
||||
}
|
||||
|
||||
const isTodayTs = (ms: number) => new Date(ms).toDateString() === new Date().toDateString();
|
||||
|
||||
export const tsLabel = (ms: number) =>
|
||||
echarts.format.formatTime(isTodayTs(ms) ? 'hh:mm:ss' : 'hh:mm:ss\ndd-MM-yyyy', ms);
|
||||
|
||||
export function buildCandles(src: CandleRow[]): ResultCandle[] {
|
||||
const arr = (TESTING_MODE ? (testCandles as CandleRow[]) : src).map(
|
||||
(c) =>
|
||||
[
|
||||
parseInt(c.timestamp, 10),
|
||||
c.shadow_top || 0,
|
||||
c.shadow_bottom || 0,
|
||||
c.body_first || 0,
|
||||
c.body_second || 0,
|
||||
] as ResultCandle,
|
||||
);
|
||||
|
||||
return arr
|
||||
.filter((e) => e[0])
|
||||
.map((e) => {
|
||||
for (let i = 1; i < 5; i++) if (d(e[i]).lessThan(0.00001)) e[i] = 0;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
export function zoomStartByPeriod(period: CandleChartProps['period']) {
|
||||
const now = Date.now();
|
||||
const hr = 3600_000;
|
||||
switch (period) {
|
||||
// case '1sec':
|
||||
// return now - 1_000;
|
||||
case '1min':
|
||||
return now - hr / 60;
|
||||
case '5min':
|
||||
return now - 5 * (hr / 60);
|
||||
case '15min':
|
||||
return now - 15 * (hr / 60);
|
||||
case '30min':
|
||||
return now - 30 * (hr / 60);
|
||||
case '1h':
|
||||
return now - hr * 24;
|
||||
case '4h':
|
||||
return now - hr * 4;
|
||||
case '1d':
|
||||
return now - hr * 24 * 7;
|
||||
case '1w':
|
||||
return now - hr * 24 * 7 * 4;
|
||||
case '1m':
|
||||
return now - hr * 24 * 7 * 52;
|
||||
default:
|
||||
return now - hr;
|
||||
}
|
||||
}
|
||||
|
||||
export function pickWindowIndices(timestamps: number[], startTs: number) {
|
||||
if (!timestamps.length) return { startIdx: 0, endIdx: 0 };
|
||||
const nearest = timestamps.reduce(
|
||||
(a, b) => (Math.abs(b - startTs) < Math.abs(a - startTs) ? b : a),
|
||||
timestamps[0],
|
||||
);
|
||||
return {
|
||||
startIdx: timestamps.indexOf(nearest),
|
||||
endIdx: timestamps.length - 1,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import LabeledInputProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps';
|
||||
import { classes } 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 = '', currency = '', value, readonly, 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="0.00"
|
||||
value={value}
|
||||
readOnly={readonly}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
|
||||
<div className={styles.labeledInput__currency} ref={labelRef}>
|
||||
<p>{currency}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LabeledInput;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
.labeledInput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-family: 500;
|
||||
line-height: 100%;
|
||||
color: var(--table-th-color);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: var(--bordered-input-bg);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--window-border-color);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: #1f8feb;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
border-color: #ff6767;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__currency {
|
||||
max-width: 150px;
|
||||
padding: 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> p {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 100%;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.labeledInput {
|
||||
gap: 5px;
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
input {
|
||||
padding: 11px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__currency {
|
||||
padding: 0 8px;
|
||||
|
||||
> p {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
src/components/dex/InputPanelItem/index.tsx
Normal file
275
src/components/dex/InputPanelItem/index.tsx
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import { Store } from '@/store/store-reducer';
|
||||
import { createOrder } from '@/utils/methods';
|
||||
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, notationToString } from '@/utils/utils';
|
||||
import InputPanelItemProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps';
|
||||
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 { useAlert } from '@/hook/useAlert';
|
||||
import { buySellValues } from '@/constants';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import styles from './styles.module.scss';
|
||||
import LabeledInput from './components/LabeledInput';
|
||||
|
||||
function InputPanelItem(props: InputPanelItemProps) {
|
||||
const {
|
||||
priceState = '',
|
||||
amountState = '',
|
||||
totalState = '',
|
||||
buySellState = buySellValues[0],
|
||||
setBuySellState,
|
||||
setPriceFunction,
|
||||
setAmountFunction,
|
||||
setRangeInputValue,
|
||||
rangeInputValue = '50',
|
||||
balance = 0,
|
||||
zanoBalance = 0,
|
||||
amountValid,
|
||||
priceValid,
|
||||
totalValid,
|
||||
totalUsd,
|
||||
scrollToOrderList,
|
||||
currencyNames,
|
||||
onAfter,
|
||||
} = props;
|
||||
|
||||
const { state } = useContext(Store);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { setAlertState, setAlertSubtitle } = useAlert();
|
||||
const [creatingState, setCreatingState] = useState(false);
|
||||
const { firstCurrencyName, secondCurrencyName } = currencyNames;
|
||||
|
||||
const [hasImmediateMatch, setHasImmediateMatch] = useState(false);
|
||||
const isBuy = buySellState?.code === 'buy';
|
||||
|
||||
function goToSuitableTab() {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('tab', 'matches');
|
||||
|
||||
router.replace(`${pathname}?${params.toString()}`, undefined, {
|
||||
shallow: true,
|
||||
scroll: false,
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setPriceFunction('');
|
||||
setAmountFunction('');
|
||||
setRangeInputValue('50');
|
||||
}
|
||||
|
||||
async function postOrder() {
|
||||
const price = new Decimal(priceState);
|
||||
const amount = new Decimal(amountState);
|
||||
const total = new Decimal(totalState);
|
||||
|
||||
const isFull =
|
||||
price.greaterThan(0) &&
|
||||
price.lessThan(1000000000) &&
|
||||
amount.greaterThan(0) &&
|
||||
amount.lessThan(1000000000) &&
|
||||
total.greaterThan(0);
|
||||
|
||||
if (!isFull) return;
|
||||
|
||||
if (isBuy) {
|
||||
const zanoAmount = new Decimal(zanoBalance);
|
||||
if (zanoAmount.lessThan(total)) {
|
||||
setAlertState('error');
|
||||
setAlertSubtitle('Insufficient ZANO balance');
|
||||
setTimeout(() => setAlertState(null), 3000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const assetAmount = new Decimal(balance);
|
||||
if (assetAmount.lessThan(amount)) {
|
||||
setAlertState('error');
|
||||
setAlertSubtitle(`Insufficient ${firstCurrencyName} balance`);
|
||||
setTimeout(() => setAlertState(null), 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const orderData: CreateOrderData = {
|
||||
type: isBuy ? 'buy' : 'sell',
|
||||
side: 'limit',
|
||||
price: price.toString(),
|
||||
amount: amount.toString(),
|
||||
pairId: typeof router.query.id === 'string' ? router.query.id : '',
|
||||
};
|
||||
|
||||
setCreatingState(true);
|
||||
const result = await createOrder(orderData);
|
||||
setCreatingState(false);
|
||||
|
||||
if (result.success) {
|
||||
if (result.data?.immediateMatch) {
|
||||
setHasImmediateMatch(true);
|
||||
}
|
||||
onAfter();
|
||||
resetForm();
|
||||
} else {
|
||||
setAlertState('error');
|
||||
if (result.data === 'Same order') {
|
||||
setAlertSubtitle('Order already exists');
|
||||
} else {
|
||||
setAlertSubtitle('Failed to create order');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setAlertState(null);
|
||||
setAlertSubtitle('');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function onRangeInput(e: ChangeEvent<HTMLInputElement>) {
|
||||
setRangeInputValue(e.target.value);
|
||||
if (balance > 0) {
|
||||
const rangeValue = new Decimal(e.target.value || '0');
|
||||
const balanceDecimal = new Decimal(balance);
|
||||
const calculatedAmount = balanceDecimal.mul(rangeValue.div(100)).toString();
|
||||
setAmountFunction(calculatedAmount || '');
|
||||
}
|
||||
}
|
||||
|
||||
const buttonText = creatingState ? 'Creating...' : 'Create Order';
|
||||
const isButtonDisabled = !priceValid || !amountValid || !totalValid || creatingState;
|
||||
const showTotalError = priceState !== '' && amountState !== '' && !totalValid;
|
||||
|
||||
return (
|
||||
<div className={styles.inputPanel}>
|
||||
{hasImmediateMatch && (
|
||||
<Alert
|
||||
type="custom"
|
||||
customContent={
|
||||
<div className={styles.applyAlert}>
|
||||
<Image src={infoIcon} alt="success" width={64} height={64} />
|
||||
<div className={styles.applyAlert__content}>
|
||||
<h2>Apply the order</h2>
|
||||
<p>You have to apply the order</p>
|
||||
<Button
|
||||
className={styles.applyAlert__button}
|
||||
onClick={() => {
|
||||
scrollToOrderList();
|
||||
goToSuitableTab();
|
||||
setHasImmediateMatch(false);
|
||||
}}
|
||||
>
|
||||
Apply now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
close={() => setHasImmediateMatch(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.inputPanel__header}>
|
||||
<h5 className={styles.title}>Trade</h5>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputPanel__body}>
|
||||
<div className={styles.inputPanel__selector}>
|
||||
<button
|
||||
onClick={() => setBuySellState(buySellValues[1])}
|
||||
className={classes(
|
||||
styles.inputPanel__selector_item,
|
||||
buySellState.code === 'buy' && styles.buy,
|
||||
)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBuySellState(buySellValues[2])}
|
||||
className={classes(
|
||||
styles.inputPanel__selector_item,
|
||||
buySellState.code === 'sell' && styles.sell,
|
||||
)}
|
||||
>
|
||||
Sell
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<LabeledInput
|
||||
value={priceState}
|
||||
setValue={setPriceFunction}
|
||||
currency={secondCurrencyName}
|
||||
label="Price"
|
||||
invalid={!!priceState && !priceValid}
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
value={amountState}
|
||||
setValue={setAmountFunction}
|
||||
currency={firstCurrencyName}
|
||||
label="Quantity"
|
||||
invalid={!!amountState && !amountValid}
|
||||
/>
|
||||
|
||||
<div className={classes(isBuy && styles.disabled)}>
|
||||
<RangeInput value={!isBuy ? rangeInputValue : '50'} onInput={onRangeInput} />
|
||||
<div className={styles.inputPanel__body_labels}>
|
||||
<p className={styles.inputPanel__body_labels__item}>0%</p>
|
||||
<p className={styles.inputPanel__body_labels__item}>100%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputPanel__body_labels}>
|
||||
<p className={styles.inputPanel__body_labels__item}>
|
||||
Available <span className={styles.balance}>Balance</span>
|
||||
</p>
|
||||
<p className={styles.inputPanel__body_labels__item}>
|
||||
<span>{balance || 0}</span> {firstCurrencyName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputPanel__body_total}>
|
||||
<LabeledInput
|
||||
value={amountState && priceState && notationToString(totalState)}
|
||||
setValue={() => undefined}
|
||||
currency={secondCurrencyName}
|
||||
label="Total"
|
||||
readonly={true}
|
||||
invalid={showTotalError}
|
||||
/>
|
||||
|
||||
<div className={classes(styles.inputPanel__body_labels, styles.mobileWrap)}>
|
||||
<p className={styles.inputPanel__body_labels__item}>
|
||||
Fee: <span>0.01</span> ZANO
|
||||
</p>
|
||||
|
||||
<p className={styles.inputPanel__body_labels__item}>
|
||||
~ ${formatDollarValue(totalUsd || '0')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{state.wallet?.connected ? (
|
||||
<Button
|
||||
disabled={isButtonDisabled}
|
||||
onClick={postOrder}
|
||||
className={classes(
|
||||
styles.inputPanel__body_btn,
|
||||
isBuy ? styles.buy : styles.sell,
|
||||
)}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
) : (
|
||||
<ConnectButton className={styles.inputPanel__body_btn} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InputPanelItem;
|
||||
202
src/components/dex/InputPanelItem/styles.module.scss
Normal file
202
src/components/dex/InputPanelItem/styles.module.scss
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
.inputPanel {
|
||||
width: 100%;
|
||||
height: 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: 12px;
|
||||
border-bottom: 1px solid var(--delimiter-color);
|
||||
margin-bottom: 10px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__selector {
|
||||
display: flex;
|
||||
background-color: var(--selector-bg-color);
|
||||
padding: 4px;
|
||||
border-radius: 20px;
|
||||
|
||||
&_item {
|
||||
cursor: pointer;
|
||||
width: 50%;
|
||||
background-color: transparent;
|
||||
border-radius: 100px;
|
||||
padding-block: 9px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.buy {
|
||||
background-color: #16d1d6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.sell {
|
||||
background-color: #ff6767;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
&_labels {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__item {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
color: var(--table-th-color);
|
||||
|
||||
.balance {
|
||||
color: var(--table-th-color);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
&_btn {
|
||||
margin-top: 10px;
|
||||
|
||||
&.buy {
|
||||
background-color: #16d1d6;
|
||||
|
||||
&:hover {
|
||||
background-color: #45dade;
|
||||
}
|
||||
}
|
||||
|
||||
&.sell {
|
||||
background-color: #ff6767;
|
||||
|
||||
&:hover {
|
||||
background-color: #ff8585;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.inputPanel {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__selector {
|
||||
padding: 0;
|
||||
|
||||
&_item {
|
||||
padding-block: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
gap: 10px;
|
||||
|
||||
&_labels {
|
||||
&.mobileWrap {
|
||||
gap: 10px;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.inputPanel__body_labels__item:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__item,
|
||||
&__item span {
|
||||
font-size: 10px;
|
||||
|
||||
.balance {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_btn {
|
||||
margin-top: 0;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/components/dex/MatrixConnectionBadge/index.tsx
Normal file
88
src/components/dex/MatrixConnectionBadge/index.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import Tooltip from '@/components/UI/Tooltip/Tooltip';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ReactComponent as ConnectionIcon } from '@/assets/images/UI/connection.svg';
|
||||
import { classes } from '@/utils/utils';
|
||||
import styles from './styles.module.scss';
|
||||
import { MatrixConnectionBadgeProps } from './types';
|
||||
|
||||
function MatrixConnectionBadge({
|
||||
userAdress,
|
||||
userAlias,
|
||||
matrixAddresses,
|
||||
isSm,
|
||||
}: MatrixConnectionBadgeProps) {
|
||||
const hasConnection = (address: string) =>
|
||||
matrixAddresses.some((item) => item.address === address && item.registered);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
||||
const anchorRef = useRef<HTMLParagraphElement | null>(null);
|
||||
|
||||
const updatePosition = () => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setPos({
|
||||
top: rect.bottom + 8,
|
||||
left: rect.left + rect.width / 2,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
updatePosition();
|
||||
const onScrollOrResize = () => updatePosition();
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScrollOrResize, true);
|
||||
window.removeEventListener('resize', onScrollOrResize);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (!userAdress || !hasConnection(userAdress)) return <></>;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.badge, isSm && styles.sm)}>
|
||||
<p
|
||||
ref={anchorRef}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(`https://matrix.to/#/@${userAlias}:zano.org`);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setOpen(true);
|
||||
requestAnimationFrame(updatePosition);
|
||||
}}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
className={styles.badge__link}
|
||||
>
|
||||
<ConnectionIcon />
|
||||
</p>
|
||||
|
||||
{open &&
|
||||
pos &&
|
||||
createPortal(
|
||||
<Tooltip
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
className={styles.badge__tooltip}
|
||||
arrowClass={styles.badge__tooltip_arrow}
|
||||
shown={true}
|
||||
>
|
||||
<p className={styles.badge__tooltip_text}>Matrix connection</p>
|
||||
</Tooltip>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MatrixConnectionBadge;
|
||||
36
src/components/dex/MatrixConnectionBadge/styles.module.scss
Normal file
36
src/components/dex/MatrixConnectionBadge/styles.module.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.badge {
|
||||
position: relative;
|
||||
|
||||
&.sm {
|
||||
.badge__link svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
margin-top: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
padding: 10px;
|
||||
background-color: var(--trade-table-tooltip);
|
||||
font-size: 12px;
|
||||
|
||||
&_text {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
&_arrow {
|
||||
border-radius: 2px;
|
||||
left: 50%;
|
||||
background-color: var(--trade-table-tooltip) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/components/dex/MatrixConnectionBadge/types.ts
Normal file
8
src/components/dex/MatrixConnectionBadge/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import MatrixAddress from '@/interfaces/common/MatrixAddress';
|
||||
|
||||
export interface MatrixConnectionBadgeProps {
|
||||
isSm?: boolean;
|
||||
userAdress?: string;
|
||||
userAlias?: string;
|
||||
matrixAddresses: MatrixAddress[];
|
||||
}
|
||||
52
src/components/dex/OrderRowTooltipCell/index.tsx
Normal file
52
src/components/dex/OrderRowTooltipCell/index.tsx
Normal 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;
|
||||
28
src/components/dex/OrderRowTooltipCell/styles.module.scss
Normal file
28
src/components/dex/OrderRowTooltipCell/styles.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
9
src/components/dex/OrderRowTooltipCell/types.ts
Normal file
9
src/components/dex/OrderRowTooltipCell/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export interface OrderRowTooltipCellProps {
|
||||
style?: React.CSSProperties;
|
||||
children: string | ReactNode;
|
||||
sideText?: string;
|
||||
sideTextColor?: string;
|
||||
noTooltip?: boolean;
|
||||
}
|
||||
70
src/components/dex/OrdersPool/columns/index.tsx
Normal file
70
src/components/dex/OrdersPool/columns/index.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { formatTimestamp, notationToString } from '@/utils/utils';
|
||||
import { ColumnDef } from '@/components/default/GenericTable/types';
|
||||
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
|
||||
import { Trade } from '@/interfaces/responses/trades/GetTradeRes';
|
||||
import { BuildColumnsArgs } from './types';
|
||||
import TotalUsdCell from '../../TotalUsdCell';
|
||||
import styles from '../styles.module.scss';
|
||||
|
||||
export function buildOrderPoolColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
}: BuildColumnsArgs): ColumnDef<PageOrderData>[] {
|
||||
return [
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '80px',
|
||||
cell: (row) => (
|
||||
<p style={{ color: row.type === 'buy' ? '#16D1D6' : '#FF6767' }}>
|
||||
{notationToString(row.price, 8)}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Qty ({firstCurrencyName})</>,
|
||||
width: '80px',
|
||||
cell: (row) => <p>{notationToString(row.left, 8)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
header: <>Total ({secondCurrencyName})</>,
|
||||
width: '80px',
|
||||
align: 'right',
|
||||
className: styles.hideTotalSm,
|
||||
cell: (row) => <TotalUsdCell amount={row.left} price={row.price} fixed={8} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildTradesColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
}: BuildColumnsArgs): ColumnDef<Trade>[] {
|
||||
return [
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '80px',
|
||||
cell: (row) => (
|
||||
<p style={{ color: row.type === 'buy' ? '#16D1D6' : '#FF6767' }}>
|
||||
{notationToString(row.price)}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Qty ({firstCurrencyName})</>,
|
||||
width: '80px',
|
||||
cell: (row) => <p>{notationToString(row.amount)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
align: 'right',
|
||||
header: <>Time</>,
|
||||
width: '80px',
|
||||
cell: (row) => <p>{formatTimestamp(row.timestamp)}</p>,
|
||||
},
|
||||
];
|
||||
}
|
||||
4
src/components/dex/OrdersPool/columns/types.ts
Normal file
4
src/components/dex/OrdersPool/columns/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface BuildColumnsArgs {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
}
|
||||
408
src/components/dex/OrdersPool/index.tsx
Normal file
408
src/components/dex/OrdersPool/index.tsx
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
classes,
|
||||
createOrderSorter,
|
||||
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 { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
|
||||
import useMouseLeave from '@/hook/useMouseLeave';
|
||||
import { tabsType } from '@/components/UI/Tabs/types';
|
||||
import Tabs from '@/components/UI/Tabs';
|
||||
import GenericTable from '@/components/default/GenericTable';
|
||||
import styles from './styles.module.scss';
|
||||
import BadgeStatus from '../BadgeStatus';
|
||||
import { OrdersPoolProps } from './types';
|
||||
import { buildOrderPoolColumns, buildTradesColumns } from './columns';
|
||||
|
||||
const tabsData: tabsType[] = [
|
||||
{
|
||||
title: 'Order Pool',
|
||||
type: 'orders',
|
||||
},
|
||||
{
|
||||
title: 'Recent Trades',
|
||||
type: 'trades',
|
||||
},
|
||||
];
|
||||
|
||||
const OrdersPool = (props: OrdersPoolProps) => {
|
||||
const {
|
||||
ordersBuySell,
|
||||
setOrdersBuySell,
|
||||
currencyNames,
|
||||
ordersLoading,
|
||||
ordersHistory,
|
||||
filteredOrdersHistory,
|
||||
secondAssetUsdPrice,
|
||||
takeOrderClick,
|
||||
trades,
|
||||
tradesLoading,
|
||||
} = props;
|
||||
const ordersInfoRef = useRef<HTMLTableSectionElement>(null);
|
||||
const scrollRef = useRef<HTMLTableSectionElement>(null);
|
||||
const ordersMiddleRef = useRef<HTMLDivElement>(null);
|
||||
const { firstCurrencyName, secondCurrencyName } = currencyNames;
|
||||
const [infoTooltipPos, setInfoTooltipPos] = useState({ x: 0, y: 0 });
|
||||
const [ordersInfoTooltip, setOrdersInfoTooltip] = useState<PageOrderData | null>(null);
|
||||
const [currentOrder, setCurrentOrder] = useState<tabsType>(tabsData[0]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
let buyTotal = new Decimal(0);
|
||||
let sellTotal = new Decimal(0);
|
||||
let maxBuyRow = new Decimal(0);
|
||||
let maxSellRow = new Decimal(0);
|
||||
|
||||
for (const o of ordersHistory) {
|
||||
const qty = new Decimal(o.amount || 0);
|
||||
const price = new Decimal(o.price || 0);
|
||||
const rowTotal = qty.mul(price);
|
||||
|
||||
if (o.type === 'buy') {
|
||||
buyTotal = buyTotal.plus(rowTotal);
|
||||
if (rowTotal.gt(maxBuyRow)) maxBuyRow = rowTotal;
|
||||
} else if (o.type === 'sell') {
|
||||
sellTotal = sellTotal.plus(rowTotal);
|
||||
if (rowTotal.gt(maxSellRow)) maxSellRow = rowTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const totalZano = buyTotal.plus(sellTotal);
|
||||
const pct = (part: Decimal, whole: Decimal) =>
|
||||
whole.gt(0) ? part.mul(100).div(whole) : new Decimal(0);
|
||||
|
||||
const buyPct = pct(buyTotal, totalZano);
|
||||
const sellPct = pct(sellTotal, totalZano);
|
||||
|
||||
return {
|
||||
buyTotal,
|
||||
sellTotal,
|
||||
totalZano,
|
||||
buyPct,
|
||||
sellPct,
|
||||
maxBuyRow,
|
||||
maxSellRow,
|
||||
};
|
||||
}, [ordersHistory]);
|
||||
|
||||
const toDisplayPair = (buyPctDec: Decimal, sellPctDec: Decimal) => {
|
||||
const MIN_DISPLAY_PCT = 1;
|
||||
const buyRaw = buyPctDec.toNumber();
|
||||
const sellRaw = sellPctDec.toNumber();
|
||||
|
||||
if (!Number.isFinite(buyRaw) || !Number.isFinite(sellRaw)) return { buy: 0, sell: 0 };
|
||||
|
||||
if (buyRaw === 0 && sellRaw === 0) return { buy: 0, sell: 0 };
|
||||
if (buyRaw === 0) return { buy: 0, sell: 100 };
|
||||
if (sellRaw === 0) return { buy: 100, sell: 0 };
|
||||
|
||||
let buyDisp = Math.floor(buyRaw);
|
||||
let sellDisp = Math.floor(sellRaw);
|
||||
|
||||
if (buyDisp < MIN_DISPLAY_PCT) buyDisp = MIN_DISPLAY_PCT;
|
||||
if (sellDisp < MIN_DISPLAY_PCT) sellDisp = MIN_DISPLAY_PCT;
|
||||
|
||||
const diff = 100 - (buyDisp + sellDisp);
|
||||
if (diff !== 0) {
|
||||
if (buyRaw >= sellRaw) buyDisp += diff;
|
||||
else sellDisp += diff;
|
||||
}
|
||||
|
||||
buyDisp = Math.max(0, Math.min(100, buyDisp));
|
||||
sellDisp = Math.max(0, Math.min(100, sellDisp));
|
||||
|
||||
return { buy: buyDisp, sell: sellDisp };
|
||||
};
|
||||
|
||||
const { buy: buyDisp, sell: sellDisp } = toDisplayPair(totals.buyPct, totals.sellPct);
|
||||
|
||||
const moveInfoTooltip = (event: React.MouseEvent) => {
|
||||
setInfoTooltipPos({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
const ordersPool = useMemo(
|
||||
() =>
|
||||
buildOrderPoolColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
}),
|
||||
[firstCurrencyName, secondCurrencyName],
|
||||
);
|
||||
|
||||
const tradeOrders = useMemo(
|
||||
() =>
|
||||
buildTradesColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
}),
|
||||
[firstCurrencyName, secondCurrencyName],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
const parent = scrollRef.current;
|
||||
|
||||
if (ordersBuySell.code === 'all' && ordersMiddleRef.current) {
|
||||
const child = ordersMiddleRef.current;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const childRect = child.getBoundingClientRect();
|
||||
|
||||
const scrollTop =
|
||||
childRect.top -
|
||||
parentRect.top +
|
||||
parent.scrollTop -
|
||||
parent.clientHeight / 2 +
|
||||
childRect.height / 2;
|
||||
|
||||
parent.scrollTop = Math.round(scrollTop);
|
||||
} else {
|
||||
parent.scrollTop = 0;
|
||||
}
|
||||
}, [ordersLoading, filteredOrdersHistory.length, ordersBuySell.code]);
|
||||
|
||||
const sortedTrades = createOrderSorter<PageOrderData>({
|
||||
getPrice: (e) => e.price,
|
||||
getSide: (e) => e.type,
|
||||
});
|
||||
|
||||
const renderTable = () => {
|
||||
switch (currentOrder.type) {
|
||||
case 'orders':
|
||||
return (
|
||||
<>
|
||||
{!ordersLoading ? (
|
||||
<div onMouseMove={moveInfoTooltip} ref={ordersInfoRef}>
|
||||
<GenericTable
|
||||
className={styles.ordersPool__content_orders}
|
||||
tableClassName={styles.table}
|
||||
tbodyClassName={styles.table__body}
|
||||
theadClassName={styles.table__header}
|
||||
columns={ordersPool}
|
||||
data={filteredOrdersHistory.sort(sortedTrades)}
|
||||
getRowKey={(r) => r.id}
|
||||
groupBy={(r) => r.type}
|
||||
scrollRef={scrollRef}
|
||||
renderGroupHeader={({ groupKey }) => {
|
||||
if (groupKey === 'buy') {
|
||||
return (
|
||||
<div ref={ordersMiddleRef} style={{ height: 0 }} />
|
||||
);
|
||||
}
|
||||
}}
|
||||
getRowProps={(row) => {
|
||||
const rowTotalZano = new Decimal(row.left || 0).mul(
|
||||
new Decimal(row.price || 0),
|
||||
);
|
||||
const denom =
|
||||
row.type === 'buy'
|
||||
? totals.maxBuyRow
|
||||
: totals.maxSellRow;
|
||||
const widthPct = denom.gt(0)
|
||||
? rowTotalZano.mul(100).div(denom)
|
||||
: new Decimal(0);
|
||||
|
||||
return {
|
||||
className: styles[row.type],
|
||||
style: {
|
||||
'--precentage': `${widthPct.toDecimalPlaces(2).toString()}%`,
|
||||
} as React.CSSProperties,
|
||||
onClick: (event) => takeOrderClick(event, row),
|
||||
onMouseMove: (event) => {
|
||||
const tr = event.target as HTMLElement;
|
||||
if (tr.classList.contains('alias'))
|
||||
setOrdersInfoTooltip(null);
|
||||
},
|
||||
onMouseEnter: () => setOrdersInfoTooltip(row),
|
||||
onMouseLeave: () => setOrdersInfoTooltip(null),
|
||||
};
|
||||
}}
|
||||
responsive={{
|
||||
query: '(max-width: 640px)',
|
||||
hiddenKeys: ['total'],
|
||||
alignOverride: { quantity: 'right' },
|
||||
tableLayout: 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ContentPreloader style={{ marginTop: 40 }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case 'trades':
|
||||
return (
|
||||
<>
|
||||
{!tradesLoading ? (
|
||||
<GenericTable
|
||||
className={classes(styles.ordersPool__content_orders, styles.full)}
|
||||
tableClassName={styles.table}
|
||||
tbodyClassName={styles.table__body}
|
||||
theadClassName={styles.table__header}
|
||||
columns={tradeOrders}
|
||||
data={trades.slice(0, 100)}
|
||||
getRowKey={(r) => r.id}
|
||||
/>
|
||||
) : (
|
||||
<ContentPreloader style={{ marginTop: 40 }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useMouseLeave(ordersInfoRef, () => setOrdersInfoTooltip(null));
|
||||
return (
|
||||
<>
|
||||
<div className={styles.ordersPool}>
|
||||
<div className={styles.ordersPool__header}>
|
||||
<Tabs value={currentOrder} setValue={setCurrentOrder} data={tabsData} />
|
||||
|
||||
{currentOrder.type === 'orders' && (
|
||||
<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}>
|
||||
{renderTable()}
|
||||
|
||||
{currentOrder.type === 'orders' && !ordersLoading && totals.totalZano.gt(0) && (
|
||||
<div className={styles.ordersPool__content_stats}>
|
||||
<div
|
||||
style={{ '--width': `${buyDisp}%` } as React.CSSProperties}
|
||||
className={classes(styles.stat_item, styles.buy)}
|
||||
>
|
||||
<div className={styles.stat_item__badge}>B</div>
|
||||
{buyDisp}%
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ '--width': `${sellDisp}%` } as React.CSSProperties}
|
||||
className={classes(styles.stat_item, styles.sell)}
|
||||
>
|
||||
{sellDisp}%<div className={styles.stat_item__badge}>S</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</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',
|
||||
}}
|
||||
>
|
||||
{ordersInfoTooltip?.price}
|
||||
</p>
|
||||
<span>
|
||||
~
|
||||
{secondAssetUsdPrice && ordersInfoTooltip?.price !== undefined
|
||||
? (() => {
|
||||
const total = new Decimal(secondAssetUsdPrice).mul(
|
||||
ordersInfoTooltip.price,
|
||||
);
|
||||
|
||||
if (total.abs().lt(0.01)) {
|
||||
return `$${total
|
||||
.toFixed(8)
|
||||
.replace(/(\.\d*?[1-9])0+$/, '$1')
|
||||
.replace(/\.0+$/, '')}`;
|
||||
}
|
||||
|
||||
return `$${total.toFixed(2).replace(/\.0+$/, '')}`;
|
||||
})()
|
||||
: 'undefined'}
|
||||
</span>
|
||||
|
||||
<h6>Amount ({firstCurrencyName})</h6>
|
||||
<p>{notationToString(ordersInfoTooltip?.left)}</p>
|
||||
|
||||
<h6>Total ({secondCurrencyName})</h6>
|
||||
<p>{notationToString(totalDecimal.toString())}</p>
|
||||
<span>
|
||||
~{' '}
|
||||
{totalValue ? `$${formatDollarValue(totalValue)}` : 'undefined'}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersPool;
|
||||
265
src/components/dex/OrdersPool/styles.module.scss
Normal file
265
src/components/dex/OrdersPool/styles.module.scss
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
.ordersPool {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
background: var(--window-bg-color);
|
||||
border: 1px solid var(--delimiter-color);
|
||||
border-radius: 10px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
|
||||
&_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;
|
||||
opacity: 60%;
|
||||
|
||||
&.selected,
|
||||
&:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
&.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;
|
||||
width: 100%;
|
||||
|
||||
&_orders {
|
||||
width: 100%;
|
||||
height: 410px;
|
||||
|
||||
&.full {
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
th {
|
||||
color: var(--table-th-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--table-tr-hover-color);
|
||||
}
|
||||
|
||||
&.buy {
|
||||
td {
|
||||
&:last-child {
|
||||
&::before {
|
||||
background-color: var(--dex-buy-percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sell {
|
||||
td {
|
||||
&:last-child {
|
||||
&::before {
|
||||
background-color: var(--dex-sell-percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
position: static !important;
|
||||
|
||||
&:last-child {
|
||||
&::before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: var(--precentage);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_stats {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 30px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1px;
|
||||
|
||||
.stat_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 100%;
|
||||
font-weight: 400;
|
||||
min-width: 60px;
|
||||
|
||||
&.buy {
|
||||
width: var(--width);
|
||||
background-color: var(--dex-buy-percentage);
|
||||
color: #16d1d6;
|
||||
|
||||
.stat_item__badge {
|
||||
background-color: #16d1d6;
|
||||
}
|
||||
}
|
||||
|
||||
&.sell {
|
||||
width: var(--width);
|
||||
background-color: var(--dex-sell-percentage);
|
||||
justify-content: flex-end;
|
||||
color: #ff6767;
|
||||
|
||||
.stat_item__badge {
|
||||
background-color: #ff6767;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 120%;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
border: 1px solid var(--dex-tooltip-border-color);
|
||||
min-width: 140px;
|
||||
max-width: 300px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.ordersPool {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding-top: 0;
|
||||
|
||||
&_orders {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
&_stats {
|
||||
margin-top: 22px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
23
src/components/dex/OrdersPool/types.ts
Normal file
23
src/components/dex/OrdersPool/types.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
|
||||
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
|
||||
import { Trade } from '@/interfaces/responses/trades/GetTradeRes';
|
||||
|
||||
export interface OrdersPoolProps {
|
||||
ordersBuySell: SelectValue;
|
||||
secondAssetUsdPrice: number | undefined;
|
||||
setOrdersBuySell: Dispatch<SetStateAction<SelectValue>>;
|
||||
currencyNames: {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
};
|
||||
ordersLoading: boolean;
|
||||
ordersHistory: PageOrderData[];
|
||||
filteredOrdersHistory: PageOrderData[];
|
||||
trades: Trade[];
|
||||
tradesLoading: boolean;
|
||||
takeOrderClick: (
|
||||
_event: React.MouseEvent<HTMLTableRowElement, MouseEvent>,
|
||||
_e: PageOrderData,
|
||||
) => void;
|
||||
}
|
||||
25
src/components/dex/TotalUsdCell/index.tsx
Normal file
25
src/components/dex/TotalUsdCell/index.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useMemo } from 'react';
|
||||
import Decimal from 'decimal.js';
|
||||
import { notationToString, formatDollarValue, classes } from '@/utils/utils';
|
||||
import { TotalUsdCellProps } from './types';
|
||||
|
||||
export default function TotalUsdCell({
|
||||
amount,
|
||||
price,
|
||||
secondAssetUsdPrice,
|
||||
fixed,
|
||||
className,
|
||||
}: TotalUsdCellProps) {
|
||||
const total = useMemo(
|
||||
() => new Decimal(amount || 0).mul(new Decimal(price || 0)),
|
||||
[amount, price],
|
||||
);
|
||||
const usd = secondAssetUsdPrice ? total.mul(secondAssetUsdPrice).toFixed(2) : undefined;
|
||||
|
||||
return (
|
||||
<p className={classes(className)}>
|
||||
{notationToString((fixed ? total.toFixed(fixed) : total).toString())}{' '}
|
||||
{secondAssetUsdPrice && <span>~ ${usd && formatDollarValue(usd)}</span>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
7
src/components/dex/TotalUsdCell/types.ts
Normal file
7
src/components/dex/TotalUsdCell/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface TotalUsdCellProps {
|
||||
amount: string | number;
|
||||
price: string | number;
|
||||
secondAssetUsdPrice?: number;
|
||||
fixed?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.asset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
|
||||
&__name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__address {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export interface AssetRowProps {
|
||||
name: string;
|
||||
link: string;
|
||||
id: string;
|
||||
code: string | undefined | null;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import Image from 'next/image';
|
||||
import { getAssetIcon } from '@/utils/utils';
|
||||
import { CurrencyIconProps } from './types';
|
||||
|
||||
const CurrencyIcon = ({ code, size = 50 }: CurrencyIconProps) => (
|
||||
<Image width={size} height={size} src={getAssetIcon(String(code))} alt="currency" />
|
||||
);
|
||||
|
||||
export default CurrencyIcon;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export interface CurrencyIconProps {
|
||||
code: string | undefined | null;
|
||||
size?: number;
|
||||
}
|
||||
|
|
@ -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__top}>
|
||||
<Img />
|
||||
<p className={styles.statItem__top_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;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
.statItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
svg {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&_title {
|
||||
color: var(--footer-selected-link);
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&_val {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&_coefficient {
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.green {
|
||||
color: #16d1d6;
|
||||
}
|
||||
|
||||
&.red {
|
||||
color: #ff6767;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/components/dex/TradingHeader/index.tsx
Normal file
135
src/components/dex/TradingHeader/index.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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 { roundTo, notationToString, classes } from '@/utils/utils';
|
||||
// import questionIcon from '@/assets/images/UI/question.svg';
|
||||
// import Image from 'next/image';
|
||||
import styles from './styles.module.scss';
|
||||
import StatItem from './components/StatItem';
|
||||
import { TradingHeaderProps } from './types';
|
||||
import CurrencyIcon from './components/CurrencyIcon';
|
||||
import AssetRow from './components/AssetRow';
|
||||
|
||||
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 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), 4)}`,
|
||||
coefficient: coefficientOutput,
|
||||
},
|
||||
{
|
||||
Img: UpIcon,
|
||||
title: '24h high',
|
||||
value: `${roundTo(notationToString(pairStats?.high || 0), 4)}`,
|
||||
},
|
||||
{
|
||||
Img: DownIcon,
|
||||
title: '24h low',
|
||||
value: `${roundTo(notationToString(pairStats?.low || 0), 4)}`,
|
||||
},
|
||||
{
|
||||
Img: VolumeIcon,
|
||||
title: `24h volume (${firstCurrencyName})`,
|
||||
value: `${roundTo(notationToString(pairStats?.volume || 0), 4)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.header__stats}>
|
||||
<div className={styles.header__currency}>
|
||||
<div className={styles.header__currency_icon}>
|
||||
<CurrencyIcon code={firstAssetId} />
|
||||
</div>
|
||||
|
||||
<div className={styles.header__currency_item}>
|
||||
<p className={styles.currencyName}>
|
||||
{!pairData ? (
|
||||
'...'
|
||||
) : (
|
||||
<>
|
||||
{firstCurrencyName}
|
||||
<span>/{secondCurrencyName}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className={styles.price}>
|
||||
<p
|
||||
className={classes(
|
||||
styles.price__secondCurrency,
|
||||
coefficientOutput >= 0 ? styles.green : styles.red,
|
||||
)}
|
||||
>
|
||||
{roundTo(notationToString(pairStats?.rate || 0, 8))}
|
||||
</p>
|
||||
{pairRateUsd && <p className={styles.price__usd}>~ ${pairRateUsd}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pairData && firstAssetLink && secondAssetLink && (
|
||||
<div className={styles.header__stats_assets}>
|
||||
<AssetRow
|
||||
name={firstCurrencyName}
|
||||
link={firstAssetLink}
|
||||
id={firstAssetId || ''}
|
||||
code={firstAssetId}
|
||||
/>
|
||||
<AssetRow
|
||||
name={secondCurrencyName}
|
||||
link={secondAssetLink}
|
||||
id={secondAssetId || ''}
|
||||
code={secondAssetId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.map(({ Img, title, value, coefficient }) => (
|
||||
<StatItem
|
||||
key={title}
|
||||
className={styles.header__stats_item}
|
||||
Img={Img}
|
||||
title={title}
|
||||
value={value}
|
||||
coefficient={coefficient}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.header__actions}>
|
||||
{/* <button className={styles.header__actions_guide}>
|
||||
<Image src={questionIcon} width={24} height={24} alt="guide" />
|
||||
</button> */}
|
||||
|
||||
<BackButton className={styles.header__actions_backBtn} isSm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradingHeader;
|
||||
172
src/components/dex/TradingHeader/styles.module.scss
Normal file
172
src/components/dex/TradingHeader/styles.module.scss
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
position: relative;
|
||||
border: none;
|
||||
|
||||
&__currency {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--delimiter-color);
|
||||
|
||||
&_icon {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--icon-bg-color);
|
||||
border-radius: 50%;
|
||||
|
||||
> img {
|
||||
width: 24px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&_item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
|
||||
.currencyName {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
color: var(--footer-selected-link);
|
||||
}
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&__secondCurrency {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
|
||||
&.green {
|
||||
color: #16d1d6;
|
||||
}
|
||||
|
||||
&.red {
|
||||
color: #ff6767;
|
||||
}
|
||||
}
|
||||
|
||||
&__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);
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
&_guide {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1180px) {
|
||||
.header {
|
||||
&__currency {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&__stats_assets {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 980px) {
|
||||
.header {
|
||||
&__currency {
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--delimiter-color);
|
||||
}
|
||||
|
||||
&__stats_item {
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
|
||||
&:last-child {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions_backBtn {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 0 !important;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.header__stats_item {
|
||||
&:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 580px) {
|
||||
.header {
|
||||
&__currency {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&__stats_item {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/components/dex/TradingHeader/types.ts
Normal file
12
src/components/dex/TradingHeader/types.ts
Normal 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;
|
||||
}
|
||||
251
src/components/dex/UserOrders/cards/UniversalCards/index.tsx
Normal file
251
src/components/dex/UserOrders/cards/UniversalCards/index.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import React from 'react';
|
||||
import { classes, formatTimestamp, notationToString } from '@/utils/utils';
|
||||
import TotalUsdCell from '@/components/dex/TotalUsdCell';
|
||||
import EmptyMessage from '@/components/UI/EmptyMessage';
|
||||
import AliasCell from '@/components/dex/AliasCell';
|
||||
import { UniversalCardsProps } from './types';
|
||||
import styles from '../styles.module.scss';
|
||||
import RequestActionCell from '../../components/RequestActionCell';
|
||||
import CancelActionCell from '../../components/CancelActionCell';
|
||||
|
||||
const OffersRow = ({
|
||||
matches,
|
||||
requests,
|
||||
offers,
|
||||
mobile,
|
||||
}: {
|
||||
matches: number;
|
||||
requests: number;
|
||||
offers: number;
|
||||
mobile?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className={classes(
|
||||
styles.card__row,
|
||||
styles.sm,
|
||||
styles.card__offers,
|
||||
mobile && styles.mobile,
|
||||
)}
|
||||
>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Matches</p>
|
||||
<p
|
||||
className={classes(
|
||||
styles.card__value,
|
||||
matches > 0 ? styles.primary : styles.secondary,
|
||||
)}
|
||||
>
|
||||
{matches}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Requests</p>
|
||||
<p
|
||||
className={classes(
|
||||
styles.card__value,
|
||||
requests > 0 ? styles.primary : styles.secondary,
|
||||
)}
|
||||
>
|
||||
{requests}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Offers</p>
|
||||
<p
|
||||
className={classes(
|
||||
styles.card__value,
|
||||
offers > 0 ? styles.primary : styles.secondary,
|
||||
)}
|
||||
>
|
||||
{offers}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AliasRow = ({ label = 'Alias', children }: { label?: string; children: React.ReactNode }) => (
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>{label}</p>
|
||||
<div className={styles.card__value}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function UniversalCards(props: UniversalCardsProps) {
|
||||
const { firstCurrencyName, secondCurrencyName, secondAssetUsdPrice, data, onAfter } = props;
|
||||
|
||||
if (!data?.length)
|
||||
return (
|
||||
<div className={styles.cards}>
|
||||
<EmptyMessage text="No data" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.cards}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{data.map((row: any) => {
|
||||
const side: 'buy' | 'sell' = row.type ?? row.creator;
|
||||
|
||||
const Header = (
|
||||
<div className={styles.card__row}>
|
||||
<div className={classes(styles.card__col, styles.card__pair)}>
|
||||
<p className={styles.card__label}>Pair</p>
|
||||
<p className={styles.card__value}>
|
||||
{firstCurrencyName}/{secondCurrencyName}
|
||||
<span className={styles.card__type}>{side}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={classes(styles.card__col, styles.card__col_direction)}>
|
||||
<p className={styles.card__label}>Direction</p>
|
||||
<p className={classes(styles.card__value, styles.card__type)}>{side}</p>
|
||||
</div>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Time</p>
|
||||
<p className={styles.card__value}>
|
||||
{formatTimestamp(Number(row.timestamp))}
|
||||
</p>
|
||||
</div>
|
||||
<div className={classes(styles.card__col, styles.card__price)}>
|
||||
<p className={styles.card__label}>Price ({secondCurrencyName})</p>
|
||||
<p className={styles.card__value}>{notationToString(row.price)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Metrics = (
|
||||
<>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Quantity ({firstCurrencyName})</p>
|
||||
<p className={styles.card__value}>
|
||||
{notationToString(row.amount ?? row.left)}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.card__col}>
|
||||
<p className={styles.card__label}>Total ({secondCurrencyName})</p>
|
||||
<TotalUsdCell
|
||||
className={styles.card__value}
|
||||
amount={row.amount ?? row.left}
|
||||
price={row.price}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classes(styles.card__col, styles.card__price, styles.mobile)}
|
||||
>
|
||||
<p className={styles.card__label}>Price ({secondCurrencyName})</p>
|
||||
<p className={styles.card__value}>{notationToString(row.price)}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
let MiddleBlock: React.ReactNode = null;
|
||||
let BottomRowLeft: React.ReactNode = null;
|
||||
let Actions: React.ReactNode = null;
|
||||
|
||||
if (props.type === 'orders') {
|
||||
const id = String(row.id);
|
||||
const matches = props.matchesCountByOrderId[id] ?? 0;
|
||||
const requests = props.requestsCountByOrderId[id] ?? 0;
|
||||
const offers = props.offersCountByOrderId[id] ?? 0;
|
||||
|
||||
MiddleBlock = (
|
||||
<OffersRow matches={matches} requests={requests} offers={offers} />
|
||||
);
|
||||
BottomRowLeft = (
|
||||
<OffersRow matches={matches} requests={requests} offers={offers} mobile />
|
||||
);
|
||||
Actions = (
|
||||
<div className={styles.card__actions}>
|
||||
<CancelActionCell id={row.id} onAfter={onAfter} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'matches' || props.type === 'offers') {
|
||||
MiddleBlock = null;
|
||||
BottomRowLeft = (
|
||||
<AliasRow>
|
||||
<AliasCell
|
||||
alias={row.user?.alias}
|
||||
address={row.user?.address}
|
||||
matrixAddresses={props.matrixAddresses}
|
||||
isInstant={row.isInstant}
|
||||
/>
|
||||
</AliasRow>
|
||||
);
|
||||
|
||||
const connectedOrder = props.userOrders?.find(
|
||||
(o) => String(o.id) === String(row.connected_order_id),
|
||||
);
|
||||
|
||||
Actions = (
|
||||
<div className={styles.card__actions}>
|
||||
<RequestActionCell
|
||||
type={props.type === 'matches' ? 'request' : 'accept'}
|
||||
row={row}
|
||||
pairData={props.pairData}
|
||||
connectedOrder={connectedOrder}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
{props.type === 'offers' && (
|
||||
<CancelActionCell
|
||||
type="reject"
|
||||
id={row.connected_order_id}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'requests') {
|
||||
MiddleBlock = null;
|
||||
BottomRowLeft = (
|
||||
<AliasRow>
|
||||
<AliasCell
|
||||
alias={row.finalizer?.alias}
|
||||
address={row.finalizer?.address}
|
||||
matrixAddresses={props.matrixAddresses}
|
||||
/>
|
||||
</AliasRow>
|
||||
);
|
||||
|
||||
Actions = (
|
||||
<div className={styles.card__actions}>
|
||||
<CancelActionCell
|
||||
id={String(
|
||||
row.creator === 'sell' ? row.sell_order_id : row.buy_order_id,
|
||||
)}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === 'history') {
|
||||
MiddleBlock = null;
|
||||
BottomRowLeft = null;
|
||||
Actions = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={row.id} className={classes(styles.card, styles[side])}>
|
||||
{Header}
|
||||
|
||||
<div className={styles.card__flex}>
|
||||
<div className={styles.card__row}>
|
||||
{Metrics}
|
||||
{MiddleBlock}
|
||||
</div>
|
||||
|
||||
<div className={styles.card__row}>
|
||||
{BottomRowLeft}
|
||||
{Actions}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/dex/UserOrders/cards/UniversalCards/types.ts
Normal file
40
src/components/dex/UserOrders/cards/UniversalCards/types.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import OrderRow from '@/interfaces/common/OrderRow';
|
||||
import ApplyTip from '@/interfaces/common/ApplyTip';
|
||||
import UserPendingType from '@/interfaces/common/UserPendingType';
|
||||
import { UserOrderData } from '@/interfaces/responses/orders/GetUserOrdersRes';
|
||||
import MatrixAddress from '@/interfaces/common/MatrixAddress';
|
||||
import PairData from '@/interfaces/common/PairData';
|
||||
|
||||
export type CardsType = 'orders' | 'matches' | 'offers' | 'requests' | 'history';
|
||||
|
||||
type Common = {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
secondAssetUsdPrice?: number;
|
||||
onAfter: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type UniversalCardsProps =
|
||||
| (Common & {
|
||||
type: 'orders';
|
||||
data: OrderRow[] | UserOrderData[];
|
||||
matchesCountByOrderId: Record<string, number>;
|
||||
requestsCountByOrderId: Record<string, number>;
|
||||
offersCountByOrderId: Record<string, number>;
|
||||
})
|
||||
| (Common & {
|
||||
type: 'matches' | 'offers';
|
||||
data: ApplyTip[];
|
||||
matrixAddresses: MatrixAddress[];
|
||||
userOrders: OrderRow[] | undefined;
|
||||
pairData: PairData | null;
|
||||
})
|
||||
| (Common & {
|
||||
type: 'requests';
|
||||
data: UserPendingType[];
|
||||
matrixAddresses: MatrixAddress[];
|
||||
})
|
||||
| (Common & {
|
||||
type: 'history';
|
||||
data: UserOrderData[];
|
||||
});
|
||||
206
src/components/dex/UserOrders/cards/styles.module.scss
Normal file
206
src/components/dex/UserOrders/cards/styles.module.scss
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
.cards {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 6px 10px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.sell {
|
||||
&::before {
|
||||
background-color: #ff6767;
|
||||
}
|
||||
|
||||
.card__type {
|
||||
color: #ff6767;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
&.buy {
|
||||
&::before {
|
||||
background-color: #16d1d6;
|
||||
}
|
||||
|
||||
.card__type {
|
||||
color: #16d1d6;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
|
||||
&.sm {
|
||||
gap: 18px;
|
||||
|
||||
.card__col {
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: var(--table-th-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
|
||||
&.primary {
|
||||
color: #1f8feb;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--table-th-color);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--table-th-color);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__col {
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__pair {
|
||||
.card__value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card__type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__flex {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.cards .card {
|
||||
gap: 10px;
|
||||
|
||||
&__row {
|
||||
gap: 15px;
|
||||
|
||||
&.sm {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__col {
|
||||
min-width: 80px;
|
||||
|
||||
&_direction {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&__pair {
|
||||
.card__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card__value {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card__type {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.cards .card {
|
||||
&__row {
|
||||
justify-content: space-between;
|
||||
|
||||
&.sm {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&__col {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&__flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__price {
|
||||
display: none;
|
||||
|
||||
&.mobile {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__offers {
|
||||
display: none;
|
||||
|
||||
&.mobile {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
369
src/components/dex/UserOrders/columns/index.tsx
Normal file
369
src/components/dex/UserOrders/columns/index.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { notationToString, formatTimestamp } from '@/utils/utils';
|
||||
import type OrderRow from '@/interfaces/common/OrderRow';
|
||||
import type { ColumnDef } from '@/components/default/GenericTable/types';
|
||||
import ApplyTip from '@/interfaces/common/ApplyTip';
|
||||
import UserPendingType from '@/interfaces/common/UserPendingType';
|
||||
import { UserOrderData } from '@/interfaces/responses/orders/GetUserOrdersRes';
|
||||
import CancelActionCell from '../components/CancelActionCell';
|
||||
import AliasCell from '../../AliasCell';
|
||||
import TotalUsdCell from '../../TotalUsdCell';
|
||||
import RequestActionCell from '../components/RequestActionCell';
|
||||
import {
|
||||
BuildApplyTipsColumnsArgs,
|
||||
BuildMyRequestsColumnsArgs,
|
||||
BuildOrderHistoryColumnsArgs,
|
||||
BuildUserColumnsArgs,
|
||||
} from './types';
|
||||
|
||||
export function buildUserColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
secondAssetUsdPrice,
|
||||
matchesCountByOrderId,
|
||||
offersCountByOrderId,
|
||||
requestsCountByOrderId,
|
||||
onAfter,
|
||||
}: BuildUserColumnsArgs): ColumnDef<OrderRow>[] {
|
||||
return [
|
||||
{
|
||||
key: 'pair',
|
||||
header: 'Pair',
|
||||
width: '120px',
|
||||
cell: (row) => (
|
||||
<p
|
||||
style={
|
||||
{
|
||||
'--direction-color': row.type === 'buy' ? '#16D1D6' : '#FF6767',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{firstCurrencyName}/{secondCurrencyName}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'direction',
|
||||
header: 'Direction',
|
||||
width: '110px',
|
||||
cell: (row) => (
|
||||
<p
|
||||
style={{
|
||||
color: row.type === 'buy' ? '#16D1D6' : '#FF6767',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{row.type}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '150px',
|
||||
cell: (row) => <p>{notationToString(row.price)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Quantity ({firstCurrencyName})</>,
|
||||
width: '160px',
|
||||
cell: (row) => <p>{notationToString(row.left)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
header: <>Total ({secondCurrencyName})</>,
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<TotalUsdCell
|
||||
amount={row.left}
|
||||
price={row.price}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'matches',
|
||||
header: 'Matches',
|
||||
width: '70px',
|
||||
cell: (row) => {
|
||||
const count = matchesCountByOrderId[row.id] ?? 0;
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: count > 0 ? '#1F8FEB' : '#B6B6C4',
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'requests',
|
||||
header: 'Requests',
|
||||
width: '70px',
|
||||
cell: (row) => {
|
||||
const count = requestsCountByOrderId[row.id] ?? 0;
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: count > 0 ? '#1F8FEB' : '#B6B6C4',
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'offers',
|
||||
header: 'Offers',
|
||||
width: '70px',
|
||||
cell: (row) => {
|
||||
const count = offersCountByOrderId[row.id] ?? 0;
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: count > 0 ? '#1F8FEB' : '#B6B6C4',
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
header: 'Time',
|
||||
width: '180px',
|
||||
cell: (row) => <p>{formatTimestamp(row.timestamp)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
width: '80px',
|
||||
align: 'left',
|
||||
cell: (row) => <CancelActionCell id={row.id} onAfter={onAfter} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildApplyTipsColumns({
|
||||
type,
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
userOrders,
|
||||
pairData,
|
||||
onAfter,
|
||||
}: BuildApplyTipsColumnsArgs): ColumnDef<ApplyTip>[] {
|
||||
return [
|
||||
{
|
||||
key: 'alias',
|
||||
header: 'Alias',
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<AliasCell
|
||||
alias={row.user?.alias}
|
||||
address={row.user?.address}
|
||||
matrixAddresses={matrixAddresses}
|
||||
isInstant={row.isInstant}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '150px',
|
||||
cell: (row) => <p>{notationToString(row.price)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Quantity ({firstCurrencyName})</>,
|
||||
width: '160px',
|
||||
cell: (row) => <p>{notationToString(row.left)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
header: <>Total ({secondCurrencyName})</>,
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<TotalUsdCell
|
||||
amount={row.left}
|
||||
price={row.price}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
header: 'Time',
|
||||
width: '180px',
|
||||
cell: (row) => <p>{formatTimestamp(Number(row.timestamp))}</p>,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
width: type === 'offers' ? '140px' : '90px',
|
||||
cell: (row) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<RequestActionCell
|
||||
type={type === 'suitables' ? 'request' : 'accept'}
|
||||
row={row}
|
||||
pairData={pairData}
|
||||
connectedOrder={userOrders.find((o) => o.id === row.connected_order_id)}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
{type === 'offers' && (
|
||||
<CancelActionCell
|
||||
type="reject"
|
||||
id={row.connected_order_id}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildMyRequestsColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
onAfter,
|
||||
}: BuildMyRequestsColumnsArgs): ColumnDef<UserPendingType>[] {
|
||||
return [
|
||||
{
|
||||
key: 'alias',
|
||||
header: 'Alias',
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<AliasCell
|
||||
alias={row.finalizer?.alias}
|
||||
address={row.finalizer?.address}
|
||||
matrixAddresses={matrixAddresses}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '150px',
|
||||
cell: (row) => <p>{notationToString(row.price)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Quantity ({firstCurrencyName})</>,
|
||||
width: '160px',
|
||||
cell: (row) => <p>{notationToString(row.amount)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
header: <>Total ({secondCurrencyName})</>,
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<TotalUsdCell
|
||||
amount={row.amount}
|
||||
price={row.price}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
header: 'Time',
|
||||
width: '180px',
|
||||
cell: (row) => <p>{formatTimestamp(row.timestamp)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
header: 'Action',
|
||||
width: '80px',
|
||||
align: 'left',
|
||||
cell: (row) => (
|
||||
<CancelActionCell
|
||||
id={String(row.creator === 'sell' ? row.sell_order_id : row.buy_order_id)}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildOrderHistoryColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
secondAssetUsdPrice,
|
||||
}: BuildOrderHistoryColumnsArgs): ColumnDef<UserOrderData>[] {
|
||||
return [
|
||||
{
|
||||
key: 'pair',
|
||||
header: 'Pair',
|
||||
width: '120px',
|
||||
cell: (row) => (
|
||||
<p
|
||||
style={
|
||||
{
|
||||
'--direction-color': row.type === 'buy' ? '#16D1D6' : '#FF6767',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{firstCurrencyName}/{secondCurrencyName}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'direction',
|
||||
header: 'Direction',
|
||||
width: '110px',
|
||||
cell: (row) => (
|
||||
<p
|
||||
style={{
|
||||
color: row.type === 'buy' ? '#16D1D6' : '#FF6767',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{row.type}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: <>Price ({secondCurrencyName})</>,
|
||||
width: '150px',
|
||||
cell: (row) => <p>{notationToString(row.price)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'quantity',
|
||||
header: <>Quantity ({firstCurrencyName})</>,
|
||||
width: '160px',
|
||||
cell: (row) => <p>{notationToString(row.left)}</p>,
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
header: <>Total ({secondCurrencyName})</>,
|
||||
width: '180px',
|
||||
cell: (row) => (
|
||||
<TotalUsdCell
|
||||
amount={row.left}
|
||||
price={row.price}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
header: 'Time',
|
||||
width: '100px',
|
||||
cell: (row) => <p>{formatTimestamp(row.timestamp)}</p>,
|
||||
},
|
||||
];
|
||||
}
|
||||
38
src/components/dex/UserOrders/columns/types.ts
Normal file
38
src/components/dex/UserOrders/columns/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import MatrixAddress from '@/interfaces/common/MatrixAddress';
|
||||
import OrderRow from '@/interfaces/common/OrderRow';
|
||||
import PairData from '@/interfaces/common/PairData';
|
||||
|
||||
export interface BuildUserColumnsArgs {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
secondAssetUsdPrice?: number;
|
||||
matchesCountByOrderId: Record<string, number>;
|
||||
requestsCountByOrderId: Record<string, number>;
|
||||
offersCountByOrderId: Record<string, number>;
|
||||
onAfter: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface BuildApplyTipsColumnsArgs {
|
||||
type: 'suitables' | 'offers';
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
matrixAddresses: MatrixAddress[];
|
||||
secondAssetUsdPrice?: number;
|
||||
userOrders: OrderRow[];
|
||||
pairData: PairData | null;
|
||||
onAfter: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface BuildMyRequestsColumnsArgs {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
secondAssetUsdPrice?: number;
|
||||
matrixAddresses: MatrixAddress[];
|
||||
onAfter: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface BuildOrderHistoryColumnsArgs {
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
secondAssetUsdPrice?: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { useState } from 'react';
|
||||
import { useAlert } from '@/hook/useAlert';
|
||||
import { cancelOrder } from '@/utils/methods';
|
||||
import ActionBtn from '@/components/UI/ActionBtn';
|
||||
import { CancelActionCellProps } from './types';
|
||||
|
||||
export default function CancelActionCell({ type = 'cancel', id, onAfter }: CancelActionCellProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setAlertState, setAlertSubtitle } = useAlert();
|
||||
|
||||
const onClick = async () => {
|
||||
if (loading) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await cancelOrder(id);
|
||||
if (!result.success) {
|
||||
setAlertState('error');
|
||||
setAlertSubtitle('Error while cancelling order');
|
||||
setTimeout(() => {
|
||||
setAlertState(null);
|
||||
setAlertSubtitle('');
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
await onAfter();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionBtn
|
||||
variant={type === 'reject' ? 'danger' : 'primary'}
|
||||
disabled={loading}
|
||||
onClick={() => onClick()}
|
||||
>
|
||||
{type === 'cancel' ? 'Cancel' : 'Reject'}
|
||||
</ActionBtn>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export interface CancelActionCellProps {
|
||||
type?: 'cancel' | 'reject';
|
||||
id: string;
|
||||
onAfter: () => Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { classes, formatTimestamp, notationToString } from '@/utils/utils';
|
||||
import { OrderGroupHeaderProps } from './types';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export default function OrderGroupHeader({
|
||||
order,
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
}: OrderGroupHeaderProps) {
|
||||
if (!order) return;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.header, styles[order.type])}>
|
||||
<div className={styles.header__item}>
|
||||
<p className={styles.header__label}>For order</p>
|
||||
|
||||
<p className={classes(styles.header__value, styles.bold)}>
|
||||
{firstCurrencyName}/{secondCurrencyName}
|
||||
</p>
|
||||
|
||||
<p className={styles.header__type}>{order.type}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.header__item}>
|
||||
<p className={styles.header__label}>Quantity</p>
|
||||
|
||||
<p className={styles.header__value}>
|
||||
{notationToString(order.left)} {firstCurrencyName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.header__item}>
|
||||
<p className={styles.header__label}>Total</p>
|
||||
|
||||
<p className={styles.header__value}>
|
||||
{notationToString(order.total)} {secondCurrencyName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.header__item}>
|
||||
<p className={styles.header__value}>{formatTimestamp(order.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
.header {
|
||||
margin-block: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding-inline: 12px;
|
||||
padding-block: 6px;
|
||||
background-color: var(--table-group-header-bg);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 14px;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&.buy {
|
||||
.header__type {
|
||||
color: #16d1d6;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: #16d1d6;
|
||||
}
|
||||
}
|
||||
|
||||
&.sell {
|
||||
.header__type {
|
||||
color: #ff6767;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: #ff6767;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__value,
|
||||
&__type {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
&.bold {
|
||||
margin-inline: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: var(--table-th-color);
|
||||
}
|
||||
|
||||
&__type {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import OrderRow from '@/interfaces/common/OrderRow';
|
||||
|
||||
export interface OrderGroupHeaderProps {
|
||||
order?: OrderRow;
|
||||
firstCurrencyName: string;
|
||||
secondCurrencyName: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import Link from 'next/link';
|
||||
import Decimal from 'decimal.js';
|
||||
import { useState, useContext } from 'react';
|
||||
import { Store } from '@/store/store-reducer';
|
||||
import { useAlert } from '@/hook/useAlert';
|
||||
import { applyOrder, confirmTransaction } from '@/utils/methods';
|
||||
import { confirmIonicSwap, ionicSwap } from '@/utils/wallet';
|
||||
import { updateAutoClosedNotification } from '@/store/actions';
|
||||
import { notationToString } from '@/utils/utils';
|
||||
import ActionBtn from '@/components/UI/ActionBtn';
|
||||
import { RequestActionCellProps } from './types';
|
||||
|
||||
export default function RequestActionCell({
|
||||
type = 'request',
|
||||
row,
|
||||
pairData,
|
||||
onAfter,
|
||||
connectedOrder,
|
||||
userOrders,
|
||||
}: RequestActionCellProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { state, dispatch } = useContext(Store);
|
||||
const { setAlertState, setAlertSubtitle } = useAlert();
|
||||
|
||||
const _connectedOrder =
|
||||
connectedOrder ?? userOrders?.find((o) => o.id === row.connected_order_id);
|
||||
|
||||
const alertErr = (subtitle: string) => {
|
||||
setAlertState('error');
|
||||
setAlertSubtitle(subtitle);
|
||||
setTimeout(() => {
|
||||
setAlertState(null);
|
||||
setAlertSubtitle('');
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const onClick = async () => {
|
||||
setLoading(true);
|
||||
|
||||
let result: { success: boolean } | null = null;
|
||||
|
||||
try {
|
||||
if (row.id) {
|
||||
updateAutoClosedNotification(dispatch, [
|
||||
...state.closed_notifications,
|
||||
parseInt(String(row.id), 10),
|
||||
]);
|
||||
}
|
||||
|
||||
if (row.transaction) {
|
||||
if (!row.hex_raw_proposal) {
|
||||
alertErr('Invalid transaction data received');
|
||||
return;
|
||||
}
|
||||
const confirmSwapResult = await confirmIonicSwap(row.hex_raw_proposal);
|
||||
if (confirmSwapResult.data?.error?.code === -7) {
|
||||
alertErr('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
if (!confirmSwapResult.data?.result) {
|
||||
alertErr('Companion responded with an error');
|
||||
return;
|
||||
}
|
||||
result = await confirmTransaction(row.id);
|
||||
} else {
|
||||
const firstCurrencyId = pairData?.first_currency.asset_id;
|
||||
const secondCurrencyId = pairData?.second_currency.asset_id;
|
||||
if (!(firstCurrencyId && secondCurrencyId)) {
|
||||
alertErr('Invalid transaction data received');
|
||||
return;
|
||||
}
|
||||
if (!_connectedOrder) return;
|
||||
|
||||
const leftDecimal = new Decimal(row.left);
|
||||
const priceDecimal = new Decimal(row.price);
|
||||
|
||||
const params = {
|
||||
destinationAssetID: row.type === 'buy' ? secondCurrencyId : firstCurrencyId,
|
||||
destinationAssetAmount: notationToString(
|
||||
row.type === 'buy'
|
||||
? leftDecimal.mul(priceDecimal).toString()
|
||||
: leftDecimal.toString(),
|
||||
),
|
||||
currentAssetID: row.type === 'buy' ? firstCurrencyId : secondCurrencyId,
|
||||
currentAssetAmount: notationToString(
|
||||
row.type === 'buy'
|
||||
? leftDecimal.toString()
|
||||
: leftDecimal.mul(priceDecimal).toString(),
|
||||
),
|
||||
destinationAddress: row.user.address,
|
||||
};
|
||||
|
||||
const createSwapResult = await ionicSwap(params);
|
||||
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({ ...row, hex_raw_proposal: hex });
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (!result) return;
|
||||
if (!result.success) {
|
||||
alertErr('Server responded with an error');
|
||||
return;
|
||||
}
|
||||
await onAfter();
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionBtn variant="success" disabled={loading} onClick={() => onClick()}>
|
||||
{type === 'request' ? 'Request' : 'Accept'}
|
||||
</ActionBtn>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import ApplyTip from '@/interfaces/common/ApplyTip';
|
||||
import OrderRow from '@/interfaces/common/OrderRow';
|
||||
import PairData from '@/interfaces/common/PairData';
|
||||
|
||||
export interface RequestActionCellProps {
|
||||
type?: 'request' | 'accept';
|
||||
row: ApplyTip;
|
||||
pairData: PairData | null;
|
||||
onAfter: () => Promise<void>;
|
||||
connectedOrder?: OrderRow;
|
||||
userOrders?: OrderRow[];
|
||||
}
|
||||
412
src/components/dex/UserOrders/index.tsx
Normal file
412
src/components/dex/UserOrders/index.tsx
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader';
|
||||
import useUpdateUser from '@/hook/useUpdateUser';
|
||||
import EmptyMessage from '@/components/UI/EmptyMessage';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import GenericTable from '@/components/default/GenericTable';
|
||||
import ActionBtn from '@/components/UI/ActionBtn';
|
||||
import { getUserOrders, getUserPendings } from '@/utils/methods';
|
||||
import UserPendingType from '@/interfaces/common/UserPendingType';
|
||||
import { Store } from '@/store/store-reducer';
|
||||
import { UserOrderData } from '@/interfaces/responses/orders/GetUserOrdersRes';
|
||||
import { useAlert } from '@/hook/useAlert';
|
||||
import Alert from '@/components/UI/Alert/Alert';
|
||||
import { tabsType } from '@/components/UI/Tabs/types';
|
||||
import Tabs from '@/components/UI/Tabs';
|
||||
import { countByKeyRecord, createOrderSorter } from '@/utils/utils';
|
||||
import ApplyTip from '@/interfaces/common/ApplyTip';
|
||||
import { useQuerySyncedTab } from '@/hook/useQuerySyncedTab';
|
||||
import { useMediaQuery } from '@/hook/useMediaQuery';
|
||||
import { UserOrdersProps } from './types';
|
||||
import styles from './styles.module.scss';
|
||||
import {
|
||||
buildApplyTipsColumns,
|
||||
buildMyRequestsColumns,
|
||||
buildOrderHistoryColumns,
|
||||
buildUserColumns,
|
||||
} from './columns';
|
||||
import OrderGroupHeader from './components/OrderGroupHeader';
|
||||
import UniversalCards from './cards/UniversalCards';
|
||||
|
||||
const UserOrders = ({
|
||||
userOrders,
|
||||
applyTips,
|
||||
myOrdersLoading,
|
||||
handleCancelAllOrders,
|
||||
orderListRef,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
onAfter,
|
||||
pairData,
|
||||
}: UserOrdersProps) => {
|
||||
const { state } = useContext(Store);
|
||||
const loggedIn = !!state.wallet?.connected;
|
||||
const { setAlertState, setAlertSubtitle, alertState, alertSubtitle } = useAlert();
|
||||
const isSm = useMediaQuery('(max-width: 820px)');
|
||||
const isMobile = useMediaQuery('(max-width: 580px)');
|
||||
|
||||
const fetchUser = useUpdateUser();
|
||||
const matches = applyTips.filter((s) => !s.transaction);
|
||||
const offers = applyTips.filter((s) => s.transaction);
|
||||
const [userRequests, setUserRequests] = useState<UserPendingType[]>([]);
|
||||
const [ordersHistory, setOrdersHistory] = useState<UserOrderData[]>([]);
|
||||
|
||||
const tabsData: tabsType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'My Orders',
|
||||
type: 'opened',
|
||||
length: userOrders.length,
|
||||
},
|
||||
{
|
||||
title: 'Matches',
|
||||
type: 'matches',
|
||||
length: matches.length,
|
||||
},
|
||||
{
|
||||
title: 'My requests',
|
||||
type: 'requests',
|
||||
length: userRequests.length,
|
||||
},
|
||||
{
|
||||
title: 'Offers',
|
||||
type: 'offers',
|
||||
length: offers.length,
|
||||
},
|
||||
{
|
||||
title: 'History',
|
||||
type: 'history',
|
||||
length: ordersHistory.length,
|
||||
},
|
||||
],
|
||||
[
|
||||
offers.length,
|
||||
userOrders.length,
|
||||
matches.length,
|
||||
userRequests.length,
|
||||
ordersHistory.length,
|
||||
],
|
||||
);
|
||||
|
||||
const { active: ordersType, setActiveTab } = useQuerySyncedTab({
|
||||
tabs: tabsData,
|
||||
defaultType: 'opened',
|
||||
queryKey: 'tab',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
|
||||
(async () => {
|
||||
const requestsData = await getUserPendings();
|
||||
|
||||
if (requestsData.success) {
|
||||
setUserRequests(requestsData.data);
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
const result = await getUserOrders();
|
||||
|
||||
if (!result.success) {
|
||||
setAlertState('error');
|
||||
setAlertSubtitle('Error loading orders data');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
setAlertState(null);
|
||||
setAlertSubtitle('');
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredOrdersHistory = result.data
|
||||
.filter((s) => s.pair_id === pairData?.id)
|
||||
.filter((s) => s.status === 'finished');
|
||||
|
||||
fetchUser();
|
||||
|
||||
setOrdersHistory(filteredOrdersHistory);
|
||||
})();
|
||||
}, [userOrders, applyTips]);
|
||||
|
||||
const firstCurrencyName = pairData?.first_currency?.name ?? '';
|
||||
const secondCurrencyName = pairData?.second_currency?.name ?? '';
|
||||
|
||||
const matchesCountByOrderId = useMemo(() => {
|
||||
return countByKeyRecord(matches, (tip) => tip.connected_order_id);
|
||||
}, [matches]);
|
||||
const requestsCountByOrderId = useMemo(() => {
|
||||
return countByKeyRecord(userRequests, (tip) =>
|
||||
tip.creator === 'sell' ? tip.sell_order_id : tip.buy_order_id,
|
||||
);
|
||||
}, [userRequests]);
|
||||
const offersCountByOrderId = useMemo(() => {
|
||||
return countByKeyRecord(offers, (tip) => tip.connected_order_id);
|
||||
}, [offers]);
|
||||
|
||||
const columnsOpened = useMemo(
|
||||
() =>
|
||||
buildUserColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
secondAssetUsdPrice,
|
||||
matchesCountByOrderId,
|
||||
requestsCountByOrderId,
|
||||
offersCountByOrderId,
|
||||
onAfter,
|
||||
}),
|
||||
[
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
secondAssetUsdPrice,
|
||||
matchesCountByOrderId,
|
||||
offersCountByOrderId,
|
||||
requestsCountByOrderId,
|
||||
onAfter,
|
||||
],
|
||||
);
|
||||
|
||||
const columnsSuitables = useMemo(
|
||||
() =>
|
||||
buildApplyTipsColumns({
|
||||
type: 'suitables',
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
userOrders,
|
||||
pairData,
|
||||
onAfter,
|
||||
}),
|
||||
[
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
userOrders,
|
||||
pairData,
|
||||
onAfter,
|
||||
],
|
||||
);
|
||||
|
||||
const columnsMyRequests = useMemo(
|
||||
() =>
|
||||
buildMyRequestsColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
onAfter,
|
||||
}),
|
||||
[firstCurrencyName, secondCurrencyName, onAfter, matrixAddresses],
|
||||
);
|
||||
|
||||
const columnsOffers = useMemo(
|
||||
() =>
|
||||
buildApplyTipsColumns({
|
||||
type: 'offers',
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
userOrders,
|
||||
pairData,
|
||||
onAfter,
|
||||
}),
|
||||
[
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
matrixAddresses,
|
||||
secondAssetUsdPrice,
|
||||
userOrders,
|
||||
pairData,
|
||||
onAfter,
|
||||
],
|
||||
);
|
||||
|
||||
const columnsOrderHistory = useMemo(
|
||||
() =>
|
||||
buildOrderHistoryColumns({
|
||||
firstCurrencyName,
|
||||
secondCurrencyName,
|
||||
secondAssetUsdPrice,
|
||||
}),
|
||||
[firstCurrencyName, secondCurrencyName, secondAssetUsdPrice],
|
||||
);
|
||||
|
||||
const sortMatches = createOrderSorter<ApplyTip>({
|
||||
getPrice: (e) => e.price,
|
||||
getSide: (e) => e.type,
|
||||
});
|
||||
|
||||
const renderOrders = () => {
|
||||
switch (ordersType.type) {
|
||||
case 'opened':
|
||||
return !isSm ? (
|
||||
<GenericTable
|
||||
className={styles.userOrders__body}
|
||||
columns={columnsOpened}
|
||||
data={userOrders}
|
||||
getRowKey={(r) => r.id}
|
||||
emptyMessage="No orders"
|
||||
/>
|
||||
) : (
|
||||
<UniversalCards
|
||||
type="orders"
|
||||
data={userOrders}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
matchesCountByOrderId={matchesCountByOrderId}
|
||||
offersCountByOrderId={offersCountByOrderId}
|
||||
requestsCountByOrderId={requestsCountByOrderId}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
);
|
||||
case 'matches':
|
||||
return !isSm ? (
|
||||
<GenericTable
|
||||
className={styles.userOrders__body}
|
||||
columns={columnsSuitables}
|
||||
data={matches.sort(sortMatches)}
|
||||
getRowKey={(r) => r.id}
|
||||
emptyMessage="No suitables"
|
||||
groupBy={(r) => r.connected_order_id}
|
||||
renderGroupHeader={({ groupKey }) => (
|
||||
<OrderGroupHeader
|
||||
order={userOrders.find((o) => String(o.id) === String(groupKey))}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<UniversalCards
|
||||
type="matches"
|
||||
data={matches.sort(sortMatches)}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
onAfter={onAfter}
|
||||
matrixAddresses={matrixAddresses}
|
||||
pairData={pairData}
|
||||
userOrders={userOrders}
|
||||
/>
|
||||
);
|
||||
case 'requests':
|
||||
return !isSm ? (
|
||||
<GenericTable
|
||||
className={styles.userOrders__body}
|
||||
columns={columnsMyRequests}
|
||||
data={userRequests}
|
||||
getRowKey={(r) => r.id}
|
||||
emptyMessage="No requests"
|
||||
groupBy={(r) => (r.creator === 'sell' ? r.sell_order_id : r.buy_order_id)}
|
||||
renderGroupHeader={({ groupKey }) => (
|
||||
<OrderGroupHeader
|
||||
order={userOrders.find((o) => String(o.id) === String(groupKey))}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<UniversalCards
|
||||
type="requests"
|
||||
data={userRequests}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
onAfter={onAfter}
|
||||
matrixAddresses={matrixAddresses}
|
||||
/>
|
||||
);
|
||||
case 'offers':
|
||||
return !isSm ? (
|
||||
<GenericTable
|
||||
className={styles.userOrders__body}
|
||||
columns={columnsOffers}
|
||||
data={offers}
|
||||
getRowKey={(r) => r.id}
|
||||
emptyMessage="No offers"
|
||||
groupBy={(r) => r.connected_order_id}
|
||||
renderGroupHeader={({ groupKey }) => (
|
||||
<OrderGroupHeader
|
||||
order={userOrders.find((o) => String(o.id) === String(groupKey))}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<UniversalCards
|
||||
type="offers"
|
||||
data={offers}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
onAfter={onAfter}
|
||||
matrixAddresses={matrixAddresses}
|
||||
pairData={pairData}
|
||||
userOrders={userOrders}
|
||||
/>
|
||||
);
|
||||
case 'history':
|
||||
return !isSm ? (
|
||||
<GenericTable
|
||||
className={styles.userOrders__body}
|
||||
columns={columnsOrderHistory}
|
||||
data={ordersHistory}
|
||||
getRowKey={(r) => r.id}
|
||||
emptyMessage="No data"
|
||||
/>
|
||||
) : (
|
||||
<UniversalCards
|
||||
type="history"
|
||||
data={ordersHistory}
|
||||
firstCurrencyName={firstCurrencyName}
|
||||
secondCurrencyName={secondCurrencyName}
|
||||
secondAssetUsdPrice={secondAssetUsdPrice}
|
||||
onAfter={onAfter}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={orderListRef} className={styles.userOrders}>
|
||||
<div className={styles.userOrders__header}>
|
||||
<Tabs
|
||||
type={isMobile ? 'button' : 'tab'}
|
||||
data={tabsData}
|
||||
value={ordersType}
|
||||
setValue={(t) => setActiveTab(t.type)}
|
||||
/>
|
||||
|
||||
{ordersType?.type === 'opened' && userOrders.length > 0 && (
|
||||
<ActionBtn
|
||||
className={styles.userOrders__header_btn}
|
||||
onClick={handleCancelAllOrders}
|
||||
>
|
||||
Cancel all
|
||||
</ActionBtn>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!myOrdersLoading && loggedIn && renderOrders()}
|
||||
|
||||
{myOrdersLoading && loggedIn && <ContentPreloader style={{ marginTop: 40 }} />}
|
||||
{!loggedIn && <EmptyMessage text="Connect wallet to see your orders" />}
|
||||
</div>
|
||||
|
||||
{alertState && (
|
||||
<Alert
|
||||
type={alertState}
|
||||
subtitle={alertSubtitle || ''}
|
||||
close={() => setAlertState(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserOrders;
|
||||
118
src/components/dex/UserOrders/styles.module.scss
Normal file
118
src/components/dex/UserOrders/styles.module.scss
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
.userOrders {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: var(--window-bg-color);
|
||||
border: 1px solid var(--delimiter-color);
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
min-height: 310px;
|
||||
|
||||
&__body {
|
||||
position: relative;
|
||||
padding-top: 5px;
|
||||
height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
padding-top: 14px;
|
||||
margin-inline: 14px;
|
||||
padding-bottom: 0;
|
||||
|
||||
&_btn {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 7px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
|
||||
thead {
|
||||
th {
|
||||
min-width: 100px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
color: var(--table-th-color);
|
||||
padding: 6px 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
position: relative;
|
||||
|
||||
> p {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
&:first-child {
|
||||
&::before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 50%;
|
||||
width: 2px;
|
||||
background-color: var(--direction-color);
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
line-height: 1;
|
||||
color: var(--font-dimmed-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.userOrders {
|
||||
&__header {
|
||||
&_btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 580px) {
|
||||
.userOrders {
|
||||
margin-top: 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
|
||||
&__header,
|
||||
&__body {
|
||||
padding: 0;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/components/dex/UserOrders/types.ts
Normal file
18
src/components/dex/UserOrders/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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 { ForwardedRef } from 'react';
|
||||
|
||||
export type OrderType = 'opened' | 'suitable' | 'requests' | 'offers' | 'history';
|
||||
export interface UserOrdersProps {
|
||||
orderListRef: ForwardedRef<HTMLDivElement>;
|
||||
userOrders: OrderRow[];
|
||||
applyTips: ApplyTip[];
|
||||
myOrdersLoading: boolean;
|
||||
handleCancelAllOrders: () => void;
|
||||
secondAssetUsdPrice: number | undefined;
|
||||
matrixAddresses: MatrixAddress[];
|
||||
pairData: PairData | null;
|
||||
onAfter: () => Promise<void>;
|
||||
}
|
||||
30
src/constants/index.ts
Normal file
30
src/constants/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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: '1s', code: '1sec' },
|
||||
{ name: '1m', code: '1min' },
|
||||
{ name: '5m', code: '5min' },
|
||||
{ name: '15m', code: '15min' },
|
||||
{ name: '30m', code: '30min' },
|
||||
{ name: '1h', code: '1h' },
|
||||
{ name: '4h', code: '4h' },
|
||||
{ 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
22
src/hook/useAlert.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
30
src/hook/useFilteredData.ts
Normal file
30
src/hook/useFilteredData.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes';
|
||||
import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue';
|
||||
import { Store } from '@/store/store-reducer';
|
||||
import { useContext } from 'react';
|
||||
|
||||
interface useFilteredDataParams {
|
||||
ordersHistory: PageOrderData[];
|
||||
ordersBuySell: SelectValue;
|
||||
}
|
||||
|
||||
const useFilteredData = ({ ordersHistory, ordersBuySell }: useFilteredDataParams) => {
|
||||
const { state } = useContext(Store);
|
||||
|
||||
const filteredOrdersHistory = ordersHistory
|
||||
?.filter((e) => (ordersBuySell.code === 'all' ? e : e.type === ordersBuySell.code))
|
||||
?.filter((e) => e.user.address !== state.wallet?.address)
|
||||
?.filter((e) => parseFloat(e.left.toString()) !== 0)
|
||||
?.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,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFilteredData;
|
||||
25
src/hook/useMatrixAddresses.ts
Normal file
25
src/hook/useMatrixAddresses.ts
Normal 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;
|
||||
19
src/hook/useMediaQuery.ts
Normal file
19
src/hook/useMediaQuery.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const media = window.matchMedia(query);
|
||||
const listener = () => setMatches(media.matches);
|
||||
|
||||
setMatches(media.matches);
|
||||
|
||||
media.addEventListener('change', listener);
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
21
src/hook/useMouseLeave.ts
Normal file
21
src/hook/useMouseLeave.ts
Normal 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;
|
||||
110
src/hook/useOrdereForm.ts
Normal file
110
src/hook/useOrdereForm.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
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 '@/utils/handleInputChange';
|
||||
|
||||
interface UseOrderFormParams {
|
||||
pairData: PairData | null;
|
||||
balance: string | undefined;
|
||||
assetsRates: Map<string, number>;
|
||||
}
|
||||
|
||||
function clamp12(str: string) {
|
||||
try {
|
||||
return new Decimal(str || '0').toDecimalPlaces(12, Decimal.ROUND_DOWN).toString();
|
||||
} catch {
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
export function useOrderForm({
|
||||
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: (v: string) => setTotal(clamp12(v)),
|
||||
setThisValid: setPriceValid,
|
||||
setTotalValid,
|
||||
});
|
||||
}
|
||||
|
||||
function onAmountChange(inputValue: string) {
|
||||
handleInputChange({
|
||||
inputValue,
|
||||
priceOrAmount: 'amount',
|
||||
otherValue: price,
|
||||
thisDP: amountDP,
|
||||
totalDP: priceDP,
|
||||
setThisState: setAmount,
|
||||
setTotalState: (v: string) => setTotal(clamp12(v)),
|
||||
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,
|
||||
};
|
||||
}
|
||||
71
src/hook/useQuerySyncedTab.ts
Normal file
71
src/hook/useQuerySyncedTab.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
type TabType = string;
|
||||
|
||||
type TabItem<T extends TabType> = {
|
||||
title: string;
|
||||
type: T;
|
||||
length?: number;
|
||||
};
|
||||
|
||||
type Options<T extends TabType> = {
|
||||
tabs: TabItem<T>[];
|
||||
queryKey?: string;
|
||||
defaultType?: T;
|
||||
replace?: boolean;
|
||||
};
|
||||
|
||||
export function useQuerySyncedTab<T extends TabType>({
|
||||
tabs,
|
||||
queryKey = 'tab',
|
||||
defaultType,
|
||||
replace = true,
|
||||
}: Options<T>) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const urlValue = searchParams.get(queryKey) as T | null;
|
||||
|
||||
const initialTab = useMemo(() => {
|
||||
const fallback = (defaultType ?? tabs[0]?.type) as T;
|
||||
if (!urlValue) return tabs.find((t) => t.type === fallback) ?? tabs[0];
|
||||
return (
|
||||
tabs.find((t) => t.type === urlValue) ??
|
||||
tabs.find((t) => t.type === fallback) ??
|
||||
tabs[0]
|
||||
);
|
||||
}, [tabs, urlValue, defaultType]);
|
||||
|
||||
const [active, setActive] = useState<TabItem<T>>(initialTab);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(initialTab);
|
||||
}, [initialTab]);
|
||||
|
||||
const setActiveTab = (next: TabItem<T> | T) => {
|
||||
const nextType = (typeof next === 'string' ? next : next.type) as T;
|
||||
|
||||
const found = tabs.find((t) => t.type === nextType);
|
||||
if (found) setActive(found);
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
const def = (defaultType ?? tabs[0]?.type) as T;
|
||||
if (nextType === def) {
|
||||
params.delete(queryKey);
|
||||
} else {
|
||||
params.set(queryKey, nextType);
|
||||
}
|
||||
|
||||
const url = params.toString() ? `${pathname}?${params}` : pathname;
|
||||
|
||||
if (replace) {
|
||||
router.replace(url, { scroll: false });
|
||||
} else {
|
||||
router.push(url, { scroll: false });
|
||||
}
|
||||
};
|
||||
|
||||
return { active, setActiveTab };
|
||||
}
|
||||
20
src/hook/useScroll.ts
Normal file
20
src/hook/useScroll.ts
Normal 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;
|
||||
85
src/hook/useSocketListeners.ts
Normal file
85
src/hook/useSocketListeners.ts
Normal 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');
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
62
src/hook/useTradeInit.ts
Normal file
62
src/hook/useTradeInit.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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 { ZANO_ASSET_ID } from '@/utils/utils';
|
||||
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 firstCurrencyAssetID = pairData?.first_currency?.asset_id;
|
||||
|
||||
const assets = state.wallet?.connected ? state.wallet?.assets || [] : [];
|
||||
const balance = assets.find((e) => e.assetId === firstCurrencyAssetID)?.balance;
|
||||
const zanoBalance = assets.find((e) => e.assetId === ZANO_ASSET_ID)?.balance || 0;
|
||||
|
||||
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(2)
|
||||
: undefined;
|
||||
|
||||
const orderForm = useOrderForm({
|
||||
pairData,
|
||||
balance,
|
||||
assetsRates: state.assetsRates,
|
||||
});
|
||||
|
||||
return {
|
||||
currencyNames,
|
||||
firstAssetLink,
|
||||
secondAssetLink,
|
||||
secondAssetUsdPrice,
|
||||
balance,
|
||||
zanoBalance,
|
||||
orderForm,
|
||||
pairRateUsd,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTradeInit;
|
||||
143
src/hook/useTradingData.ts
Normal file
143
src/hook/useTradingData.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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() {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
6
src/interfaces/common/MatrixAddress.ts
Normal file
6
src/interfaces/common/MatrixAddress.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
interface MatrixAddress {
|
||||
address: string;
|
||||
registered: boolean;
|
||||
}
|
||||
|
||||
export default MatrixAddress;
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
type Period = '1h' | '1d' | '1w' | '1m';
|
||||
type Period = '1sec' | '1min' | '5min' | '15min' | '30min' | '1h' | '4h' | '1d' | '1w' | '1m';
|
||||
|
||||
export default Period;
|
||||
|
|
|
|||
19
src/interfaces/common/UserPendingType.ts
Normal file
19
src/interfaces/common/UserPendingType.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
interface UserPendingType {
|
||||
id: number;
|
||||
amount: string;
|
||||
price: string;
|
||||
finalizer: {
|
||||
address: string;
|
||||
alias: string;
|
||||
id: number;
|
||||
order_id: number;
|
||||
};
|
||||
buy_order_id: number;
|
||||
sell_order_id: number;
|
||||
creator: 'sell' | 'buy';
|
||||
hex_raw_proposal: string;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export default UserPendingType;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue