feat: redesign and grouping orders table

This commit is contained in:
AzizbekFayziyev 2025-08-22 14:46:35 +05:00
parent 6bd495c3e9
commit d4a703f5a8
31 changed files with 1319 additions and 689 deletions

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

View file

@ -0,0 +1,34 @@
.btn {
cursor: pointer;
border: none;
outline: none;
padding: 6px 12px;
border-radius: 5px;
background: #273666;
font-size: 12px;
font-weight: 500;
line-height: 100%;
min-width: max-content;
&:disabled {
cursor: not-allowed;
opacity: 0.5 !important;
}
&:hover {
opacity: 0.8;
background: #273666;
}
&.primary {
color: #1f8feb;
}
&.success {
color: #16d1d6;
}
&.danger {
color: #ff6767;
}
}

View file

@ -0,0 +1,5 @@
import { ButtonHTMLAttributes } from 'react';
export interface ActionBtnProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'success' | 'danger';
}

View file

@ -0,0 +1,95 @@
import { classes } from '@/utils/utils';
import React from 'react';
import EmptyMessage from '@/components/UI/EmptyMessage';
import { GenericTableProps } from './types';
export default function GenericTable<T>(props: GenericTableProps<T>) {
const {
className,
tableClassName,
theadClassName,
tbodyClassName,
columns,
data,
getRowKey,
emptyMessage = 'No data',
} = props;
return (
<div className={className}>
{data.length > 0 ? (
<div className="orders-scroll" style={{ maxHeight: '100%', overflowY: 'auto' }}>
<table
className={tableClassName}
style={{
tableLayout: 'fixed',
width: '100%',
borderCollapse: 'separate',
borderSpacing: 0,
}}
>
<colgroup>
{columns.map((col) => (
<col
key={col.key}
style={col.width ? { width: col.width } : undefined}
/>
))}
</colgroup>
<thead className={theadClassName}>
<tr>
{columns.map((col) => (
<th
key={col.key}
className={col.className}
style={{
position: 'sticky',
top: 0,
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}>
{data.map((row, i) => (
<tr key={getRowKey(row, i)}>
{columns.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>
))}
</tbody>
</table>
</div>
) : (
<EmptyMessage text={emptyMessage} />
)}
</div>
);
}

View file

@ -0,0 +1,21 @@
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 GenericTableProps<T> = {
className?: string;
tableClassName?: string;
theadClassName?: string;
tbodyClassName?: string;
columns: ColumnDef<T>[];
data: T[];
getRowKey: (_row: T, _rowIndex: number) => React.Key;
emptyMessage?: string;
};

View file

@ -1,5 +1,6 @@
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ReactComponent as ConnectionIcon } from '@/assets/images/UI/connection.svg';
import styles from './styles.module.scss';
import { MatrixConnectionBadgeProps } from './types';
@ -12,29 +13,71 @@ function MatrixConnectionBadge({
const hasConnection = (address: string) =>
matrixAddresses.some((item) => item.address === address && item.registered);
const [connectionTooltip, setConnectionTooltip] = useState(false);
return userAdress && hasConnection(userAdress) ? (
const [open, setOpen] = useState(false);
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const anchorRef = useRef<HTMLAnchorElement | 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={styles.badge}>
<a
ref={anchorRef}
href={`https://matrix.to/#/@${userAlias}:zano.org`}
target="_blank"
onMouseEnter={() => setConnectionTooltip(true)}
onMouseLeave={() => setConnectionTooltip(false)}
onMouseEnter={() => {
setOpen(true);
requestAnimationFrame(updatePosition);
}}
onMouseLeave={() => setOpen(false)}
className={styles.badge__link}
>
<ConnectionIcon />
</a>
<Tooltip
className={styles.badge__tooltip}
arrowClass={styles.badge__tooltip_arrow}
shown={connectionTooltip}
>
<p className={styles.badge__tooltip_text}>Matrix connection</p>
</Tooltip>
{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>
) : (
<></>
);
}

View file

@ -7,6 +7,7 @@
}
&__tooltip {
padding: 10px;
position: absolute;
top: 30px;
left: 50%;

View file

@ -0,0 +1,386 @@
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 '../components/AliasCell';
import TotalUsdCell from '../components/TotalUsdCell';
import RequestActionCell from '../components/RequestActionCell';
import {
BuildApplyTipsColumnsArgs,
BuildMyRequestsColumnsArgs,
BuildOrderHistoryColumnsArgs,
BuildUserColumnsArgs,
} from './types';
export function buildUserColumns({
firstCurrencyName,
secondCurrencyName,
secondAssetUsdPrice,
offersCountByOrderId,
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.amount)}</p>,
},
{
key: 'total',
header: <>Total ({secondCurrencyName})</>,
width: '180px',
cell: (row) => (
<TotalUsdCell
amount={row.left}
price={row.price}
secondAssetUsdPrice={secondAssetUsdPrice}
/>
),
},
{
key: 'offers',
header: 'Offers',
width: '90px',
cell: (row) => (
<p style={{ fontWeight: 500, color: '#1F8FEB' }}>
{offersCountByOrderId.get(String(row.id)) ?? 0}
</p>
),
},
{
key: 'orderid',
header: 'Order ID',
width: '140px',
cell: (row) => <p style={{ color: '#1F8FEB' }}>{row.pair_id}</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: '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: '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: 'orderid',
header: 'Order ID',
width: '140px',
cell: (row) => <p style={{ color: '#1F8FEB' }}>{row.connected_order_id}</p>,
},
{
key: 'time',
header: 'Time',
width: '180px',
cell: (row) => <p>{formatTimestamp(Number(row.timestamp))}</p>,
},
{
key: 'action',
header: 'Action',
width: type === 'offers' ? '150px' : '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,
onAfter,
}: BuildMyRequestsColumnsArgs): ColumnDef<UserPendingType>[] {
return [
{
key: 'pair',
header: 'Pair',
width: '120px',
cell: (row) => (
<p
style={
{
'--direction-color': row.creator === 'buy' ? '#16D1D6' : '#FF6767',
} as React.CSSProperties
}
>
{firstCurrencyName}/{secondCurrencyName}
</p>
),
},
{
key: 'direction',
header: 'Direction',
width: '110px',
cell: (row) => (
<p
style={{
color: row.creator === 'buy' ? '#16D1D6' : '#FF6767',
textTransform: 'capitalize',
}}
>
{row.creator}
</p>
),
},
{
key: 'quantity',
header: <>Quantity ({firstCurrencyName})</>,
width: '160px',
cell: (row) => <p>{notationToString(row.amount)}</p>,
},
{
key: 'sell_order_id',
header: 'Sell Order ID',
width: '140px',
cell: (row) => <p style={{ color: '#1F8FEB' }}>{row.sell_order_id}</p>,
},
{
key: 'buy_order_id',
header: 'Buy Order ID',
width: '140px',
cell: (row) => <p style={{ color: '#1F8FEB' }}>{row.buy_order_id}</p>,
},
{
key: 'status',
header: 'Status',
width: '140px',
cell: (row) => <p style={{ textTransform: 'capitalize' }}>{row.status}</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={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.amount)}</p>,
},
{
key: 'total',
header: <>Total ({secondCurrencyName})</>,
width: '180px',
cell: (row) => (
<TotalUsdCell
amount={row.left}
price={row.price}
secondAssetUsdPrice={secondAssetUsdPrice}
/>
),
},
{
key: 'orderid',
header: 'Order ID',
width: '140px',
cell: (row) => <p style={{ color: '#1F8FEB' }}>{row.pair_id}</p>,
},
{
key: 'time',
header: 'Time',
width: '180px',
cell: (row) => <p>{formatTimestamp(row.timestamp)}</p>,
},
];
}

View file

@ -0,0 +1,34 @@
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;
offersCountByOrderId: Map<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;
onAfter: () => Promise<void>;
}
export interface BuildOrderHistoryColumnsArgs {
firstCurrencyName: string;
secondCurrencyName: string;
secondAssetUsdPrice?: number;
}

View file

@ -0,0 +1,47 @@
import { useState } from 'react';
import Tooltip from '@/components/UI/Tooltip/Tooltip';
import { cutAddress } from '@/utils/utils';
import MatrixConnectionBadge from '@/components/dex/MatrixConnectionBadge';
import BadgeStatus from '@/components/dex/BadgeStatus';
import styles from '../../styles.module.scss';
import { AliasCellProps } from './types';
export default function AliasCell({
alias,
address,
matrixAddresses,
isInstant,
max = 12,
}: AliasCellProps) {
const [show, setShow] = useState(false);
const display = alias ? cutAddress(alias, max) : 'no alias';
return (
<p className={styles.alias}>
<span
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
className={styles.alias__text}
>
@{display}
</span>
<MatrixConnectionBadge
userAdress={address}
userAlias={alias}
matrixAddresses={matrixAddresses}
/>
{isInstant && (
<div style={{ marginLeft: 2 }}>
<BadgeStatus type="instant" icon />
</div>
)}
{alias && alias.length > max && (
<Tooltip className={styles.tooltip} arrowClass={styles.tooltip__arrow} shown={show}>
<p className={styles.tooltip__text}>{alias}</p>
</Tooltip>
)}
</p>
);
}

View file

@ -0,0 +1,9 @@
import MatrixAddress from '@/interfaces/common/MatrixAddress';
export interface AliasCellProps {
alias?: string;
address?: string;
matrixAddresses: MatrixAddress[];
isInstant?: boolean;
max?: number;
}

View file

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

View file

@ -0,0 +1,5 @@
export interface CancelActionCellProps {
type?: 'cancel' | 'reject';
id: string;
onAfter: () => Promise<void>;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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>
);
}

View file

@ -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[];
}

View file

@ -0,0 +1,18 @@
import { useMemo } from 'react';
import Decimal from 'decimal.js';
import { notationToString, formatDollarValue } from '@/utils/utils';
import { TotalUsdCellProps } from './types';
export default function TotalUsdCell({ amount, price, secondAssetUsdPrice }: 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>
{notationToString(total.toString())} <span>~ ${usd && formatDollarValue(usd)}</span>
</p>
);
}

View file

@ -0,0 +1,5 @@
export interface TotalUsdCellProps {
amount: string | number;
price: string | number;
secondAssetUsdPrice?: number;
}

View file

@ -2,16 +2,28 @@ import { classes } from '@/utils/utils';
import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader';
import useUpdateUser from '@/hook/useUpdateUser';
import EmptyMessage from '@/components/UI/EmptyMessage';
import { useCallback, 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, UserOrdersProps } from './types';
import styles from './styles.module.scss';
import { UserOrdersProps } from './types';
import MyOrdersRow from './components/MyOrdersRow';
import MyOrdersApplyRow from './components/MyOrdersApplyRow';
import {
buildApplyTipsColumns,
buildMyRequestsColumns,
buildOrderHistoryColumns,
buildUserColumns,
} from './columns';
const UserOrders = ({
userOrders,
applyTips,
myOrdersLoading,
loggedIn,
ordersType,
setOrdersType,
handleCancelAllOrders,
@ -23,110 +35,271 @@ const UserOrders = ({
fetchTrades,
pairData,
}: UserOrdersProps) => {
const { state } = useContext(Store);
const loggedIn = !!state.wallet?.connected;
const { setAlertState, setAlertSubtitle, alertState, alertSubtitle } = useAlert();
const fetchUser = useUpdateUser();
const firstCurrencyName = pairData?.first_currency?.name || '';
const secondCurrencyName = pairData?.second_currency?.name || '';
const suitables = applyTips.filter((s) => !s.transaction);
const offers = applyTips.filter((s) => s.transaction);
const [userRequests, setUserRequests] = useState<UserPendingType[]>([]);
const [ordersHistory, setOrdersHistory] = useState<UserOrderData[]>([]);
useEffect(() => {
(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 onAfter = useCallback(async () => {
await updateOrders();
await updateUserOrders();
await fetchUser();
await fetchTrades();
}, [updateOrders, updateUserOrders, fetchUser, fetchTrades]);
const offersCountByOrderId = useMemo(() => {
const map = new Map<string, number>();
for (const tip of applyTips) {
const key = String(tip.connected_order_id);
map.set(key, (map.get(key) ?? 0) + 1);
}
return map;
}, [applyTips]);
const columnsOpened = useMemo(
() =>
buildUserColumns({
firstCurrencyName,
secondCurrencyName,
secondAssetUsdPrice,
offersCountByOrderId,
onAfter,
}),
[firstCurrencyName, secondCurrencyName, secondAssetUsdPrice, offersCountByOrderId, 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,
onAfter,
}),
[firstCurrencyName, secondCurrencyName, onAfter],
);
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 renderTable = () => {
switch (ordersType) {
case 'opened':
return (
<GenericTable
className={styles.userOrders__body}
columns={columnsOpened}
data={userOrders}
getRowKey={(r) => r.id}
emptyMessage="No orders"
/>
);
case 'suitable':
return (
<GenericTable
className={styles.userOrders__body}
columns={columnsSuitables}
data={suitables}
getRowKey={(r) => r.id}
emptyMessage="No suitables"
/>
);
case 'requests':
return (
<GenericTable
className={styles.userOrders__body}
columns={columnsMyRequests}
data={userRequests}
getRowKey={(r) => r.id}
emptyMessage="No requests"
/>
);
case 'offers':
return (
<GenericTable
className={styles.userOrders__body}
columns={columnsOffers}
data={offers}
getRowKey={(r) => r.id}
emptyMessage="No offers"
/>
);
case 'history':
return (
<GenericTable
className={styles.userOrders__body}
columns={columnsOrderHistory}
data={ordersHistory}
getRowKey={(r) => r.id}
emptyMessage="No data"
/>
);
default:
return null;
}
};
const tabsData: tabsType[] = [
{
title: 'Open Orders',
type: 'opened',
length: userOrders.length,
},
{
title: 'Suitable',
type: 'suitable',
length: suitables.length,
},
{
title: 'My requests',
type: 'requests',
length: userRequests.length,
},
{
title: 'Offers',
type: 'offers',
length: offers.length,
},
{
title: 'Order history',
type: 'history',
length: ordersHistory.length,
},
];
return (
<div ref={orderListRef} className={styles.userOrders}>
<div className={styles.userOrders__header}>
<div className={styles.userOrders__header_nav}>
<button
onClick={() => setOrdersType('opened')}
className={classes(
styles.navItem,
ordersType === 'opened' && styles.active,
)}
>
Opened orders {applyTips?.length ? <span>{applyTips?.length}</span> : ''}
</button>
<button
onClick={() => setOrdersType('history')}
className={classes(
styles.navItem,
ordersType === 'history' && styles.active,
)}
>
Orders History
</button>
</div>
<div className={styles.trading__user_cancelOrder}>
<button
className={styles.userOrders__header_btn}
onClick={handleCancelAllOrders}
>
Cancel all orders
</button>
</div>
</div>
<div>
<table>
<thead>
<tr>
<th>Alias</th>
<th>Price ({secondCurrencyName})</th>
<th>Amount ({firstCurrencyName})</th>
<th>Total ({secondCurrencyName})</th>
<th>Offers</th>
<th></th>
</tr>
</thead>
</table>
{!myOrdersLoading && loggedIn && !!userOrders.length && (
<div className={`${styles.userOrders__body} orders-scroll`}>
<table>
<tbody className={styles.incoming}>
{userOrders.map((e) => (
<MyOrdersRow
key={e.id}
orderData={e}
applyTips={applyTips}
fetchUser={fetchUser}
matrixAddresses={matrixAddresses}
secondAssetUsdPrice={secondAssetUsdPrice}
updateOrders={updateOrders}
updateUserOrders={updateUserOrders}
/>
))}
</tbody>
</table>
{!!applyTips.length && (
<table className={styles.apply}>
<tbody>
{applyTips.map((e) => (
<MyOrdersApplyRow
key={e.id}
pairData={pairData}
orderData={e}
userOrders={userOrders}
fetchTrades={fetchTrades}
fetchUser={fetchUser}
matrixAddresses={matrixAddresses}
secondAssetUsdPrice={secondAssetUsdPrice}
updateOrders={updateOrders}
updateUserOrders={updateUserOrders}
/>
))}
</tbody>
</table>
)}
<>
<div ref={orderListRef} className={styles.userOrders}>
<div className={styles.userOrders__header}>
<div className={styles.userOrders__header_nav}>
{tabsData.map((tab) => (
<button
key={tab.type}
onClick={() => setOrdersType(tab.type)}
className={classes(
styles.navItem,
ordersType === tab.type && styles.active,
)}
>
{tab.title} ({tab.length})
</button>
))}
</div>
)}
{ordersType === 'opened' && userOrders.length > 0 && (
<ActionBtn
className={styles.userOrders__header_btn}
onClick={handleCancelAllOrders}
>
Cancel all
</ActionBtn>
)}
</div>
{!myOrdersLoading && loggedIn && renderTable()}
{myOrdersLoading && loggedIn && <ContentPreloader style={{ marginTop: 40 }} />}
{!loggedIn && <EmptyMessage text="Connect wallet to see your orders" />}
{loggedIn && !userOrders.length && !myOrdersLoading && (
<EmptyMessage text="No orders" />
)}
</div>
</div>
{alertState && (
<Alert
type={alertState}
subtitle={alertSubtitle || ''}
close={() => setAlertState(null)}
/>
)}
</>
);
};

View file

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

View file

@ -4,14 +4,21 @@ import OrderRow from '@/interfaces/common/OrderRow';
import PairData from '@/interfaces/common/PairData';
import { Dispatch, ForwardedRef, SetStateAction } from 'react';
export type OrderType = 'opened' | 'suitable' | 'requests' | 'offers' | 'history';
export type tabsType = {
title: string;
type: OrderType;
length: number;
};
export interface UserOrdersProps {
orderListRef: ForwardedRef<HTMLDivElement>;
userOrders: OrderRow[];
applyTips: ApplyTip[];
myOrdersLoading: boolean;
loggedIn: boolean;
ordersType: 'opened' | 'history';
setOrdersType: Dispatch<SetStateAction<'opened' | 'history'>>;
ordersType: OrderType;
setOrdersType: Dispatch<SetStateAction<OrderType>>;
handleCancelAllOrders: () => void;
secondAssetUsdPrice: number | undefined;
updateOrders: () => Promise<void>;

View file

@ -17,7 +17,6 @@ const useTradeInit = ({ pairData, pairStats }: useTradeInitParams) => {
firstCurrencyName: pairData?.first_currency?.name || '',
secondCurrencyName: pairData?.second_currency?.name || '',
};
const loggedIn = !!state.wallet?.connected;
const assets = state.wallet?.connected ? state.wallet?.assets || [] : [];
const balance = assets.find((e) => e.ticker === currencyNames.firstCurrencyName)?.balance;
@ -57,7 +56,6 @@ const useTradeInit = ({ pairData, pairStats }: useTradeInitParams) => {
firstAssetLink,
secondAssetLink,
secondAssetUsdPrice,
loggedIn,
balance,
buyForm,
sellForm,

View file

@ -0,0 +1,12 @@
interface UserPendingType {
id: number;
amount: string;
buy_order_id: number;
sell_order_id: number;
creator: 'sell' | 'buy';
hex_raw_proposal: string;
status: string;
timestamp: string;
}
export default UserPendingType;

View file

@ -30,6 +30,7 @@ import useFilteredData from '@/hook/useFilteredData';
import useTradeInit from '@/hook/useTradeInit';
import useMatrixAddresses from '@/hook/useMatrixAddresses';
import takeOrderClick from '@/utils/takeOrderClick';
import { OrderType } from '@/components/dex/UserOrders/types';
const CHART_OPTIONS = [{ name: 'Zano Chart' }, { name: 'Trading View', disabled: true }];
const DEFAULT_CHART = CHART_OPTIONS[0];
@ -50,7 +51,7 @@ function Trading() {
const [myOrdersLoading, setMyOrdersLoading] = useState(true);
const [ordersBuySell, setOrdersBuySell] = useState(buySellValues[0]);
const [tradesType, setTradesType] = useState<'all' | 'my'>('all');
const [ordersType, setOrdersType] = useState<'opened' | 'history'>('opened');
const [ordersType, setOrdersType] = useState<OrderType>('opened');
const [pairStats, setPairStats] = useState<PairStats | null>(null);
const [applyTips, setApplyTips] = useState<ApplyTip[]>([]);
const matrixAddresses = useMatrixAddresses(ordersHistory);
@ -63,7 +64,6 @@ function Trading() {
secondAssetLink,
secondAssetUsdPrice,
balance,
loggedIn,
pairRateUsd,
} = useTradeInit({ pairData, pairStats });
@ -204,7 +204,6 @@ function Trading() {
userOrders={userOrders}
applyTips={applyTips}
myOrdersLoading={myOrdersLoading}
loggedIn={loggedIn}
ordersType={ordersType}
setOrdersType={setOrdersType}
handleCancelAllOrders={handleCancelAllOrders}
@ -215,35 +214,34 @@ function Trading() {
fetchTrades={fetchTrades}
pairData={pairData}
/>
</div>
<div ref={orderFormRef} className={styles.trading__info_createOrders}>
{['buy', 'sell'].map((type) => {
const isBuy = type === 'buy';
const form = isBuy ? buyForm : sellForm;
<div ref={orderFormRef} className={styles.trading__info_createOrders}>
{['buy', 'sell'].map((type) => {
const isBuy = type === 'buy';
const form = isBuy ? buyForm : sellForm;
return (
<InputPanelItem
key={type}
currencyNames={currencyNames}
priceState={form.price}
amountState={form.amount}
totalState={form.total}
buySellValues={buySellValues}
buySellState={isBuy ? buySellValues[1] : buySellValues[2]}
setPriceFunction={form.onPriceChange}
setAmountFunction={form.onAmountChange}
setRangeInputValue={form.setRangeInputValue}
rangeInputValue={form.rangeInputValue}
balance={Number(balance)}
priceValid={form.priceValid}
amountValid={form.amountValid}
totalValid={form.totalValid}
totalUsd={form.totalUsd}
scrollToOrderList={scrollToOrdersList}
/>
);
})}
</div>
return (
<InputPanelItem
key={type}
currencyNames={currencyNames}
priceState={form.price}
amountState={form.amount}
totalState={form.total}
buySellValues={buySellValues}
buySellState={isBuy ? buySellValues[1] : buySellValues[2]}
setPriceFunction={form.onPriceChange}
setAmountFunction={form.onAmountChange}
setRangeInputValue={form.setRangeInputValue}
rangeInputValue={form.rangeInputValue}
balance={Number(balance)}
priceValid={form.priceValid}
amountValid={form.amountValid}
totalValid={form.totalValid}
totalUsd={form.totalUsd}
scrollToOrderList={scrollToOrdersList}
/>
);
})}
</div>
{alertState && (

View file

@ -48,7 +48,6 @@
display: flex;
gap: 20px;
margin-bottom: 40px;
height: 405px;
&_createOrders {
display: flex;

View file

@ -233,9 +233,8 @@ svg {
.orders-scroll::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
background-clip: padding-box;
}
.orders-scroll::-webkit-scrollbar-thumb {

View file

@ -320,14 +320,6 @@ export async function getChatChunk(
.then((res) => res.data);
}
export async function getZanoPrice() {
return axios
.get(
'https://explorer.zano.org/api/price?asset_id=d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a',
)
.then((res) => res.data);
}
export async function getTrades(pairId: string) {
return axios
.post(`/api/orders/get-trades`, {
@ -336,6 +328,22 @@ export async function getTrades(pairId: string) {
.then((res) => res.data);
}
export async function getUserPendings() {
return axios
.post('/api/transactions/get-my-pending', {
token: sessionStorage.getItem('token'),
})
.then((res) => res.data);
}
export async function getZanoPrice() {
return axios
.get(
'https://explorer.zano.org/api/price?asset_id=d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a',
)
.then((res) => res.data);
}
export async function getMatrixAddresses(addresses: string[]) {
try {
const { data } = await axios.post('https://messenger.zano.org/api/get-addresses', {

View file

@ -144,6 +144,21 @@ export function formatTime(ts: string | number) {
return date.toLocaleTimeString('ru-RU', { hour12: false });
}
export function formatTimestamp(ms: number | string) {
if (Number.isNaN(Number(ms))) {
return 0;
}
const date = new Date(Number(ms));
const YYYY = date.getFullYear();
const MM = String(date.getMonth() + 1).padStart(2, '0');
const DD = String(date.getDate()).padStart(2, '0');
const HH = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0');
const ss = String(date.getSeconds()).padStart(2, '0');
return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
}
export function classes(...classes: (string | boolean | undefined)[]): string {
// boolean for constructions like [predicate] && [className]
return classes.filter((className) => className).join(' ');