diff --git a/public/ui/premium.svg b/public/ui/premium.svg new file mode 100644 index 0000000..4a0d159 --- /dev/null +++ b/public/ui/premium.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/UI/connection.svg b/src/assets/images/UI/connection.svg index 54066a9..f5d5c55 100644 --- a/src/assets/images/UI/connection.svg +++ b/src/assets/images/UI/connection.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/images/UI/question.svg b/src/assets/images/UI/question.svg new file mode 100644 index 0000000..7af15ce --- /dev/null +++ b/src/assets/images/UI/question.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/UI/ActionBtn/index.tsx b/src/components/UI/ActionBtn/index.tsx new file mode 100644 index 0000000..54052ea --- /dev/null +++ b/src/components/UI/ActionBtn/index.tsx @@ -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 ( + + ); +}; + +export default ActionBtn; diff --git a/src/components/UI/ActionBtn/styles.module.scss b/src/components/UI/ActionBtn/styles.module.scss new file mode 100644 index 0000000..812c234 --- /dev/null +++ b/src/components/UI/ActionBtn/styles.module.scss @@ -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; + } +} diff --git a/src/components/UI/ActionBtn/types.ts b/src/components/UI/ActionBtn/types.ts new file mode 100644 index 0000000..2d807fd --- /dev/null +++ b/src/components/UI/ActionBtn/types.ts @@ -0,0 +1,5 @@ +import { ButtonHTMLAttributes } from 'react'; + +export interface ActionBtnProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'success' | 'danger'; +} diff --git a/src/components/UI/ContentPreloader/ContentPreloader.module.scss b/src/components/UI/ContentPreloader/ContentPreloader.module.scss index 453ed49..3076015 100644 --- a/src/components/UI/ContentPreloader/ContentPreloader.module.scss +++ b/src/components/UI/ContentPreloader/ContentPreloader.module.scss @@ -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); } } diff --git a/src/components/UI/ContentPreloader/ContentPreloader.tsx b/src/components/UI/ContentPreloader/ContentPreloader.tsx index 95d129e..2fd0a1e 100644 --- a/src/components/UI/ContentPreloader/ContentPreloader.tsx +++ b/src/components/UI/ContentPreloader/ContentPreloader.tsx @@ -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 ( -
-
+
+
-

Loading...

+ +

Loading...

); diff --git a/src/components/UI/ContentPreloader/types.ts b/src/components/UI/ContentPreloader/types.ts new file mode 100644 index 0000000..8b3523d --- /dev/null +++ b/src/components/UI/ContentPreloader/types.ts @@ -0,0 +1,6 @@ +import { CSSProperties } from 'react'; + +export interface ContentPreloaderProps { + className?: string; + style?: CSSProperties; +} diff --git a/src/components/UI/EmptyMessage/index.tsx b/src/components/UI/EmptyMessage/index.tsx new file mode 100644 index 0000000..e046819 --- /dev/null +++ b/src/components/UI/EmptyMessage/index.tsx @@ -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 ( +
+ {!customIcon ? : customIcon} +
{text}
+
+ ); +}; + +export default EmptyMessage; diff --git a/src/components/UI/EmptyMessage/styles.module.scss b/src/components/UI/EmptyMessage/styles.module.scss new file mode 100644 index 0000000..8533ba5 --- /dev/null +++ b/src/components/UI/EmptyMessage/styles.module.scss @@ -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); + } +} diff --git a/src/components/UI/EmptyMessage/types.ts b/src/components/UI/EmptyMessage/types.ts new file mode 100644 index 0000000..17d350a --- /dev/null +++ b/src/components/UI/EmptyMessage/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export interface EmptyMessageProps { + text: string; + customIcon?: ReactNode; +} diff --git a/src/components/UI/HorizontalSelect/HorizontalSelect.module.scss b/src/components/UI/HorizontalSelect/HorizontalSelect.module.scss index 967b65c..8c3ca56 100644 --- a/src/components/UI/HorizontalSelect/HorizontalSelect.module.scss +++ b/src/components/UI/HorizontalSelect/HorizontalSelect.module.scss @@ -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; diff --git a/src/components/UI/HorizontalSelect/HorizontalSelect.tsx b/src/components/UI/HorizontalSelect/HorizontalSelect.tsx index c1d833f..6758c3c 100644 --- a/src/components/UI/HorizontalSelect/HorizontalSelect.tsx +++ b/src/components/UI/HorizontalSelect/HorizontalSelect.tsx @@ -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(props: HorizontalSele return (
{props.body.map((e) => (
diff --git a/src/components/UI/RangeInput/RangeInput.module.scss b/src/components/UI/RangeInput/RangeInput.module.scss index 191d792..b68e15e 100644 --- a/src/components/UI/RangeInput/RangeInput.module.scss +++ b/src/components/UI/RangeInput/RangeInput.module.scss @@ -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; - // } } diff --git a/src/components/UI/RangeInput/RangeInput.tsx b/src/components/UI/RangeInput/RangeInput.tsx index cc8f8a8..d1df0f7 100644 --- a/src/components/UI/RangeInput/RangeInput.tsx +++ b/src/components/UI/RangeInput/RangeInput.tsx @@ -38,7 +38,7 @@ function RangeInput(props: RangeInputProps) { className={styles.input__range__tooltip} shown={tooltipShown} > - {realValue}% +

{realValue}%

{ + return ( +
+ {data.map((tab) => ( + + ))} +
+ ); +}; + +export default Tabs; diff --git a/src/components/UI/Tabs/styles.module.scss b/src/components/UI/Tabs/styles.module.scss new file mode 100644 index 0000000..143ba9c --- /dev/null +++ b/src/components/UI/Tabs/styles.module.scss @@ -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; + } + } +} diff --git a/src/components/UI/Tabs/types.ts b/src/components/UI/Tabs/types.ts new file mode 100644 index 0000000..8b5dc0d --- /dev/null +++ b/src/components/UI/Tabs/types.ts @@ -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[]; +} diff --git a/src/components/default/BackButton/BackButton.module.scss b/src/components/default/BackButton/BackButton.module.scss index 4c7d844..bf7448a 100644 --- a/src/components/default/BackButton/BackButton.module.scss +++ b/src/components/default/BackButton/BackButton.module.scss @@ -1,4 +1,13 @@ .back_btn { display: flex; gap: 12px; + + &.sm { + padding: 12px 22px; + + span { + font-size: 14px; + font-weight: 500; + } + } } diff --git a/src/components/default/BackButton/BackButton.tsx b/src/components/default/BackButton/BackButton.tsx index c3b05d7..0fbd994 100644 --- a/src/components/default/BackButton/BackButton.tsx +++ b/src/components/default/BackButton/BackButton.tsx @@ -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 ( - ); } diff --git a/src/components/default/BackButton/types.ts b/src/components/default/BackButton/types.ts new file mode 100644 index 0000000..51a9baa --- /dev/null +++ b/src/components/default/BackButton/types.ts @@ -0,0 +1,4 @@ +export interface BackButtonProps { + className?: string; + isSm?: boolean; +} diff --git a/src/components/default/Footer/Footer.tsx b/src/components/default/Footer/Footer.tsx index 55099d8..134e646 100644 --- a/src/components/default/Footer/Footer.tsx +++ b/src/components/default/Footer/Footer.tsx @@ -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', diff --git a/src/components/default/GenericTable/index.tsx b/src/components/default/GenericTable/index.tsx new file mode 100644 index 0000000..a4f5123 --- /dev/null +++ b/src/components/default/GenericTable/index.tsx @@ -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(props: GenericTableProps) { + 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(); + 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 ( +
+ {data.length > 0 ? ( +
+ + + {effectiveColumns.map((col) => ( + + ))} + + + + + {effectiveColumns.map((col) => ( + + ))} + + + + + {grouped.map((group, gi) => ( + + {renderGroupHeader && ( + + + + )} + + {group.items.map((row, i) => ( + + {effectiveColumns.map((col) => ( + + ))} + + ))} + + ))} + +
+ {col.header} +
+ {renderGroupHeader({ + groupKey: group.key, + items: group.items, + index: gi, + })} +
+ {col.cell(row, i)} +
+
+ ) : ( + + )} +
+ ); +} diff --git a/src/components/default/GenericTable/types.ts b/src/components/default/GenericTable/types.ts new file mode 100644 index 0000000..90ed80f --- /dev/null +++ b/src/components/default/GenericTable/types.ts @@ -0,0 +1,42 @@ +export type Align = 'left' | 'center' | 'right'; + +export type ColumnDef = { + key: string; + header: React.ReactNode; + width?: string; + align?: Align; + className?: string; + cell: (_row: T, _rowIndex: number) => React.ReactNode; +}; + +export type RowProps = React.HTMLAttributes & { + className?: string; +}; + +export type GroupHeaderRenderArgs = { + groupKey: string; + items: T[]; + index: number; +}; + +export type GenericTableProps = { + className?: string; + tableClassName?: string; + theadClassName?: string; + tbodyClassName?: string; + columns: ColumnDef[]; + 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) => React.ReactNode; + sortGroups?: (_a: string, _b: string) => number; + responsive?: { + query: string; + hiddenKeys?: string[]; + alignOverride?: Record; + tableLayout?: 'auto' | 'fixed'; + }; + scrollRef?: React.RefObject; +}; diff --git a/src/components/default/Header/Header.module.scss b/src/components/default/Header/Header.module.scss index 34181fa..82f2991 100644 --- a/src/components/default/Header/Header.module.scss +++ b/src/components/default/Header/Header.module.scss @@ -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; diff --git a/src/components/default/Header/Header.tsx b/src/components/default/Header/Header.tsx index 1583339..86d9d8f 100644 --- a/src/components/default/Header/Header.tsx +++ b/src/components/default/Header/Header.tsx @@ -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 &&
} -
+
Zano P2P @@ -355,9 +355,11 @@ function Header() {
- +
+ {Menu()} +
diff --git a/src/components/default/Header/NavBar/NavBar.module.scss b/src/components/default/Header/NavBar/NavBar.module.scss index 87ef6fa..8b40487 100644 --- a/src/components/default/Header/NavBar/NavBar.module.scss +++ b/src/components/default/Header/NavBar/NavBar.module.scss @@ -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; diff --git a/src/components/default/Header/NavBar/NavBar.tsx b/src/components/default/Header/NavBar/NavBar.tsx index 140ace6..d9fcfab 100644 --- a/src/components/default/Header/NavBar/NavBar.tsx +++ b/src/components/default/Header/NavBar/NavBar.tsx @@ -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) {
{title}
- + ); } return (
+ ); +} + +export default InputPanelItem; diff --git a/src/components/dex/InputPanelItem/styles.module.scss b/src/components/dex/InputPanelItem/styles.module.scss new file mode 100644 index 0000000..a24daa2 --- /dev/null +++ b/src/components/dex/InputPanelItem/styles.module.scss @@ -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%; + } + } + } +} \ No newline at end of file diff --git a/src/components/dex/MatrixConnectionBadge/index.tsx b/src/components/dex/MatrixConnectionBadge/index.tsx new file mode 100644 index 0000000..1d28078 --- /dev/null +++ b/src/components/dex/MatrixConnectionBadge/index.tsx @@ -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(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 ( +
+

{ + e.preventDefault(); + window.open(`https://matrix.to/#/@${userAlias}:zano.org`); + }} + onMouseEnter={() => { + setOpen(true); + requestAnimationFrame(updatePosition); + }} + onMouseLeave={() => setOpen(false)} + className={styles.badge__link} + > + +

+ + {open && + pos && + createPortal( + +

Matrix connection

+
, + document.body, + )} +
+ ); +} + +export default MatrixConnectionBadge; diff --git a/src/components/dex/MatrixConnectionBadge/styles.module.scss b/src/components/dex/MatrixConnectionBadge/styles.module.scss new file mode 100644 index 0000000..61504d8 --- /dev/null +++ b/src/components/dex/MatrixConnectionBadge/styles.module.scss @@ -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; + } + } +} diff --git a/src/components/dex/MatrixConnectionBadge/types.ts b/src/components/dex/MatrixConnectionBadge/types.ts new file mode 100644 index 0000000..1e49526 --- /dev/null +++ b/src/components/dex/MatrixConnectionBadge/types.ts @@ -0,0 +1,8 @@ +import MatrixAddress from '@/interfaces/common/MatrixAddress'; + +export interface MatrixConnectionBadgeProps { + isSm?: boolean; + userAdress?: string; + userAlias?: string; + matrixAddresses: MatrixAddress[]; +} diff --git a/src/components/dex/OrderRowTooltipCell/index.tsx b/src/components/dex/OrderRowTooltipCell/index.tsx new file mode 100644 index 0000000..b584236 --- /dev/null +++ b/src/components/dex/OrderRowTooltipCell/index.tsx @@ -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 ( + +

setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + {children} + {sideText && ( + + {sideText} + + )} +

+ {isLongContent && !noTooltip && ( + + {tooltipText} + + )} + + ); +} + +export default OrderRowTooltipCell; diff --git a/src/components/dex/OrderRowTooltipCell/styles.module.scss b/src/components/dex/OrderRowTooltipCell/styles.module.scss new file mode 100644 index 0000000..099fc8b --- /dev/null +++ b/src/components/dex/OrderRowTooltipCell/styles.module.scss @@ -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); + } +} diff --git a/src/components/dex/OrderRowTooltipCell/types.ts b/src/components/dex/OrderRowTooltipCell/types.ts new file mode 100644 index 0000000..965bc02 --- /dev/null +++ b/src/components/dex/OrderRowTooltipCell/types.ts @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; + +export interface OrderRowTooltipCellProps { + style?: React.CSSProperties; + children: string | ReactNode; + sideText?: string; + sideTextColor?: string; + noTooltip?: boolean; +} diff --git a/src/components/dex/OrdersPool/columns/index.tsx b/src/components/dex/OrdersPool/columns/index.tsx new file mode 100644 index 0000000..86a19da --- /dev/null +++ b/src/components/dex/OrdersPool/columns/index.tsx @@ -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[] { + return [ + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '80px', + cell: (row) => ( +

+ {notationToString(row.price, 8)} +

+ ), + }, + { + key: 'quantity', + header: <>Qty ({firstCurrencyName}), + width: '80px', + cell: (row) =>

{notationToString(row.left, 8)}

, + }, + { + key: 'total', + header: <>Total ({secondCurrencyName}), + width: '80px', + align: 'right', + className: styles.hideTotalSm, + cell: (row) => , + }, + ]; +} + +export function buildTradesColumns({ + firstCurrencyName, + secondCurrencyName, +}: BuildColumnsArgs): ColumnDef[] { + return [ + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '80px', + cell: (row) => ( +

+ {notationToString(row.price)} +

+ ), + }, + { + key: 'quantity', + header: <>Qty ({firstCurrencyName}), + width: '80px', + cell: (row) =>

{notationToString(row.amount)}

, + }, + { + key: 'time', + align: 'right', + header: <>Time, + width: '80px', + cell: (row) =>

{formatTimestamp(row.timestamp)}

, + }, + ]; +} diff --git a/src/components/dex/OrdersPool/columns/types.ts b/src/components/dex/OrdersPool/columns/types.ts new file mode 100644 index 0000000..f57deed --- /dev/null +++ b/src/components/dex/OrdersPool/columns/types.ts @@ -0,0 +1,4 @@ +export interface BuildColumnsArgs { + firstCurrencyName: string; + secondCurrencyName: string; +} diff --git a/src/components/dex/OrdersPool/index.tsx b/src/components/dex/OrdersPool/index.tsx new file mode 100644 index 0000000..ff007af --- /dev/null +++ b/src/components/dex/OrdersPool/index.tsx @@ -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(null); + const scrollRef = useRef(null); + const ordersMiddleRef = useRef(null); + const { firstCurrencyName, secondCurrencyName } = currencyNames; + const [infoTooltipPos, setInfoTooltipPos] = useState({ x: 0, y: 0 }); + const [ordersInfoTooltip, setOrdersInfoTooltip] = useState(null); + const [currentOrder, setCurrentOrder] = useState(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({ + getPrice: (e) => e.price, + getSide: (e) => e.type, + }); + + const renderTable = () => { + switch (currentOrder.type) { + case 'orders': + return ( + <> + {!ordersLoading ? ( +
+ r.id} + groupBy={(r) => r.type} + scrollRef={scrollRef} + renderGroupHeader={({ groupKey }) => { + if (groupKey === 'buy') { + return ( +
+ ); + } + }} + 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', + }} + /> +
+ ) : ( + + )} + + ); + case 'trades': + return ( + <> + {!tradesLoading ? ( + r.id} + /> + ) : ( + + )} + + ); + default: + return null; + } + }; + + useMouseLeave(ordersInfoRef, () => setOrdersInfoTooltip(null)); + return ( + <> +
+
+ + + {currentOrder.type === 'orders' && ( +
+ + + + + +
+ )} +
+ +
+ {renderTable()} + + {currentOrder.type === 'orders' && !ordersLoading && totals.totalZano.gt(0) && ( +
+
+
B
+ {buyDisp}% +
+ +
+ {sellDisp}%
S
+
+
+ )} +
+
+ + {/* Order tooltip */} + {ordersInfoTooltip && + (() => { + const totalDecimal = new Decimal(ordersInfoTooltip?.left).mul( + new Decimal(ordersInfoTooltip?.price), + ); + const totalValue = secondAssetUsdPrice + ? totalDecimal.mul(secondAssetUsdPrice).toFixed(2) + : undefined; + + return ( + +
+
Alias
+

+ @{cutAddress(ordersInfoTooltip?.user?.alias || 'no alias', 12)}{' '} + {ordersInfoTooltip?.isInstant && ( + + )} +

+ +
Price ({secondCurrencyName})
+

+ {ordersInfoTooltip?.price} +

+ + ~ + {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'} + + +
Amount ({firstCurrencyName})
+

{notationToString(ordersInfoTooltip?.left)}

+ +
Total ({secondCurrencyName})
+

{notationToString(totalDecimal.toString())}

+ + ~{' '} + {totalValue ? `$${formatDollarValue(totalValue)}` : 'undefined'} + +
+
+ ); + })()} + + ); +}; + +export default OrdersPool; diff --git a/src/components/dex/OrdersPool/styles.module.scss b/src/components/dex/OrdersPool/styles.module.scss new file mode 100644 index 0000000..9e6360a --- /dev/null +++ b/src/components/dex/OrdersPool/styles.module.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/components/dex/OrdersPool/types.ts b/src/components/dex/OrdersPool/types.ts new file mode 100644 index 0000000..6260c89 --- /dev/null +++ b/src/components/dex/OrdersPool/types.ts @@ -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>; + currencyNames: { + firstCurrencyName: string; + secondCurrencyName: string; + }; + ordersLoading: boolean; + ordersHistory: PageOrderData[]; + filteredOrdersHistory: PageOrderData[]; + trades: Trade[]; + tradesLoading: boolean; + takeOrderClick: ( + _event: React.MouseEvent, + _e: PageOrderData, + ) => void; +} diff --git a/src/pages/dex/trading/TimeLeft/TimeLeft.tsx b/src/components/dex/TimeLeft/index.tsx similarity index 100% rename from src/pages/dex/trading/TimeLeft/TimeLeft.tsx rename to src/components/dex/TimeLeft/index.tsx diff --git a/src/components/dex/TotalUsdCell/index.tsx b/src/components/dex/TotalUsdCell/index.tsx new file mode 100644 index 0000000..dac3421 --- /dev/null +++ b/src/components/dex/TotalUsdCell/index.tsx @@ -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 ( +

+ {notationToString((fixed ? total.toFixed(fixed) : total).toString())}{' '} + {secondAssetUsdPrice && ~ ${usd && formatDollarValue(usd)}} +

+ ); +} diff --git a/src/components/dex/TotalUsdCell/types.ts b/src/components/dex/TotalUsdCell/types.ts new file mode 100644 index 0000000..6de879e --- /dev/null +++ b/src/components/dex/TotalUsdCell/types.ts @@ -0,0 +1,7 @@ +export interface TotalUsdCellProps { + amount: string | number; + price: string | number; + secondAssetUsdPrice?: number; + fixed?: number; + className?: string; +} diff --git a/src/components/dex/TradingHeader/components/AssetRow/index.tsx b/src/components/dex/TradingHeader/components/AssetRow/index.tsx new file mode 100644 index 0000000..dd978c7 --- /dev/null +++ b/src/components/dex/TradingHeader/components/AssetRow/index.tsx @@ -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) => ( +
+

+ {name}: +

+ + {shortenAddress(id)} + +
+); + +export default AssetRow; diff --git a/src/components/dex/TradingHeader/components/AssetRow/styles.module.scss b/src/components/dex/TradingHeader/components/AssetRow/styles.module.scss new file mode 100644 index 0000000..8ab6d64 --- /dev/null +++ b/src/components/dex/TradingHeader/components/AssetRow/styles.module.scss @@ -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; + } +} diff --git a/src/components/dex/TradingHeader/components/AssetRow/types.ts b/src/components/dex/TradingHeader/components/AssetRow/types.ts new file mode 100644 index 0000000..bb27d6b --- /dev/null +++ b/src/components/dex/TradingHeader/components/AssetRow/types.ts @@ -0,0 +1,6 @@ +export interface AssetRowProps { + name: string; + link: string; + id: string; + code: string | undefined | null; +} diff --git a/src/components/dex/TradingHeader/components/CurrencyIcon/index.tsx b/src/components/dex/TradingHeader/components/CurrencyIcon/index.tsx new file mode 100644 index 0000000..927e6bb --- /dev/null +++ b/src/components/dex/TradingHeader/components/CurrencyIcon/index.tsx @@ -0,0 +1,9 @@ +import Image from 'next/image'; +import { getAssetIcon } from '@/utils/utils'; +import { CurrencyIconProps } from './types'; + +const CurrencyIcon = ({ code, size = 50 }: CurrencyIconProps) => ( + currency +); + +export default CurrencyIcon; diff --git a/src/components/dex/TradingHeader/components/CurrencyIcon/types.ts b/src/components/dex/TradingHeader/components/CurrencyIcon/types.ts new file mode 100644 index 0000000..f181779 --- /dev/null +++ b/src/components/dex/TradingHeader/components/CurrencyIcon/types.ts @@ -0,0 +1,4 @@ +export interface CurrencyIconProps { + code: string | undefined | null; + size?: number; +} diff --git a/src/components/dex/TradingHeader/components/StatItem/index.tsx b/src/components/dex/TradingHeader/components/StatItem/index.tsx new file mode 100644 index 0000000..227551b --- /dev/null +++ b/src/components/dex/TradingHeader/components/StatItem/index.tsx @@ -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 ( +
+
+ +

{title}

+
+ +
+

{value}

+ + {coefficient !== undefined && ( +

= 0 ? styles.green : styles.red, + )} + > + {coefficient >= 0 ? '+' : ''} + {coefficient?.toFixed(2)}% +

+ )} +
+
+ ); +} + +export default StatItem; diff --git a/src/components/dex/TradingHeader/components/StatItem/styles.module.scss b/src/components/dex/TradingHeader/components/StatItem/styles.module.scss new file mode 100644 index 0000000..6d0982e --- /dev/null +++ b/src/components/dex/TradingHeader/components/StatItem/styles.module.scss @@ -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; + } + } + } +} diff --git a/src/components/dex/TradingHeader/index.tsx b/src/components/dex/TradingHeader/index.tsx new file mode 100644 index 0000000..8195d1d --- /dev/null +++ b/src/components/dex/TradingHeader/index.tsx @@ -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 ( +
+
+
+
+ +
+ +
+

+ {!pairData ? ( + '...' + ) : ( + <> + {firstCurrencyName} + /{secondCurrencyName} + + )} +

+ +
+

= 0 ? styles.green : styles.red, + )} + > + {roundTo(notationToString(pairStats?.rate || 0, 8))} +

+ {pairRateUsd &&

~ ${pairRateUsd}

} +
+
+
+ + {pairData && firstAssetLink && secondAssetLink && ( +
+ + +
+ )} + + {stats.map(({ Img, title, value, coefficient }) => ( + + ))} +
+ +
+ {/* */} + + +
+
+ ); +}; + +export default TradingHeader; diff --git a/src/components/dex/TradingHeader/styles.module.scss b/src/components/dex/TradingHeader/styles.module.scss new file mode 100644 index 0000000..d0cd246 --- /dev/null +++ b/src/components/dex/TradingHeader/styles.module.scss @@ -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; + } + } +} diff --git a/src/components/dex/TradingHeader/types.ts b/src/components/dex/TradingHeader/types.ts new file mode 100644 index 0000000..9371ec0 --- /dev/null +++ b/src/components/dex/TradingHeader/types.ts @@ -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; +} diff --git a/src/components/dex/UserOrders/cards/UniversalCards/index.tsx b/src/components/dex/UserOrders/cards/UniversalCards/index.tsx new file mode 100644 index 0000000..476c96b --- /dev/null +++ b/src/components/dex/UserOrders/cards/UniversalCards/index.tsx @@ -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; +}) => ( +
+
+

Matches

+

0 ? styles.primary : styles.secondary, + )} + > + {matches} +

+
+
+

Requests

+

0 ? styles.primary : styles.secondary, + )} + > + {requests} +

+
+
+

Offers

+

0 ? styles.primary : styles.secondary, + )} + > + {offers} +

+
+
+); + +const AliasRow = ({ label = 'Alias', children }: { label?: string; children: React.ReactNode }) => ( +
+

{label}

+
{children}
+
+); + +export default function UniversalCards(props: UniversalCardsProps) { + const { firstCurrencyName, secondCurrencyName, secondAssetUsdPrice, data, onAfter } = props; + + if (!data?.length) + return ( +
+ +
+ ); + + return ( +
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {data.map((row: any) => { + const side: 'buy' | 'sell' = row.type ?? row.creator; + + const Header = ( +
+
+

Pair

+

+ {firstCurrencyName}/{secondCurrencyName} + {side} +

+
+
+

Direction

+

{side}

+
+
+

Time

+

+ {formatTimestamp(Number(row.timestamp))} +

+
+
+

Price ({secondCurrencyName})

+

{notationToString(row.price)}

+
+
+ ); + + const Metrics = ( + <> +
+

Quantity ({firstCurrencyName})

+

+ {notationToString(row.amount ?? row.left)} +

+
+
+

Total ({secondCurrencyName})

+ +
+
+

Price ({secondCurrencyName})

+

{notationToString(row.price)}

+
+ + ); + + 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 = ( + + ); + BottomRowLeft = ( + + ); + Actions = ( +
+ +
+ ); + } + + if (props.type === 'matches' || props.type === 'offers') { + MiddleBlock = null; + BottomRowLeft = ( + + + + ); + + const connectedOrder = props.userOrders?.find( + (o) => String(o.id) === String(row.connected_order_id), + ); + + Actions = ( +
+ + {props.type === 'offers' && ( + + )} +
+ ); + } + + if (props.type === 'requests') { + MiddleBlock = null; + BottomRowLeft = ( + + + + ); + + Actions = ( +
+ +
+ ); + } + + if (props.type === 'history') { + MiddleBlock = null; + BottomRowLeft = null; + Actions = null; + } + + return ( +
+ {Header} + +
+
+ {Metrics} + {MiddleBlock} +
+ +
+ {BottomRowLeft} + {Actions} +
+
+
+ ); + })} +
+ ); +} diff --git a/src/components/dex/UserOrders/cards/UniversalCards/types.ts b/src/components/dex/UserOrders/cards/UniversalCards/types.ts new file mode 100644 index 0000000..8cecd2f --- /dev/null +++ b/src/components/dex/UserOrders/cards/UniversalCards/types.ts @@ -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; +}; + +export type UniversalCardsProps = + | (Common & { + type: 'orders'; + data: OrderRow[] | UserOrderData[]; + matchesCountByOrderId: Record; + requestsCountByOrderId: Record; + offersCountByOrderId: Record; + }) + | (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[]; + }); diff --git a/src/components/dex/UserOrders/cards/styles.module.scss b/src/components/dex/UserOrders/cards/styles.module.scss new file mode 100644 index 0000000..8919c89 --- /dev/null +++ b/src/components/dex/UserOrders/cards/styles.module.scss @@ -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; + } + } + } +} diff --git a/src/components/dex/UserOrders/columns/index.tsx b/src/components/dex/UserOrders/columns/index.tsx new file mode 100644 index 0000000..57c230a --- /dev/null +++ b/src/components/dex/UserOrders/columns/index.tsx @@ -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[] { + return [ + { + key: 'pair', + header: 'Pair', + width: '120px', + cell: (row) => ( +

+ {firstCurrencyName}/{secondCurrencyName} +

+ ), + }, + { + key: 'direction', + header: 'Direction', + width: '110px', + cell: (row) => ( +

+ {row.type} +

+ ), + }, + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '150px', + cell: (row) =>

{notationToString(row.price)}

, + }, + { + key: 'quantity', + header: <>Quantity ({firstCurrencyName}), + width: '160px', + cell: (row) =>

{notationToString(row.left)}

, + }, + { + key: 'total', + header: <>Total ({secondCurrencyName}), + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'matches', + header: 'Matches', + width: '70px', + cell: (row) => { + const count = matchesCountByOrderId[row.id] ?? 0; + + return ( +

0 ? '#1F8FEB' : '#B6B6C4', + }} + > + {count} +

+ ); + }, + }, + { + key: 'requests', + header: 'Requests', + width: '70px', + cell: (row) => { + const count = requestsCountByOrderId[row.id] ?? 0; + + return ( +

0 ? '#1F8FEB' : '#B6B6C4', + }} + > + {count} +

+ ); + }, + }, + { + key: 'offers', + header: 'Offers', + width: '70px', + cell: (row) => { + const count = offersCountByOrderId[row.id] ?? 0; + + return ( +

0 ? '#1F8FEB' : '#B6B6C4', + }} + > + {count} +

+ ); + }, + }, + { + key: 'time', + header: 'Time', + width: '180px', + cell: (row) =>

{formatTimestamp(row.timestamp)}

, + }, + { + key: 'action', + header: 'Action', + width: '80px', + align: 'left', + cell: (row) => , + }, + ]; +} + +export function buildApplyTipsColumns({ + type, + firstCurrencyName, + secondCurrencyName, + matrixAddresses, + secondAssetUsdPrice, + userOrders, + pairData, + onAfter, +}: BuildApplyTipsColumnsArgs): ColumnDef[] { + return [ + { + key: 'alias', + header: 'Alias', + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '150px', + cell: (row) =>

{notationToString(row.price)}

, + }, + { + key: 'quantity', + header: <>Quantity ({firstCurrencyName}), + width: '160px', + cell: (row) =>

{notationToString(row.left)}

, + }, + { + key: 'total', + header: <>Total ({secondCurrencyName}), + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'time', + header: 'Time', + width: '180px', + cell: (row) =>

{formatTimestamp(Number(row.timestamp))}

, + }, + { + key: 'action', + header: 'Action', + width: type === 'offers' ? '140px' : '90px', + cell: (row) => ( +
+ o.id === row.connected_order_id)} + onAfter={onAfter} + /> + {type === 'offers' && ( + + )} +
+ ), + }, + ]; +} + +export function buildMyRequestsColumns({ + firstCurrencyName, + secondCurrencyName, + matrixAddresses, + secondAssetUsdPrice, + onAfter, +}: BuildMyRequestsColumnsArgs): ColumnDef[] { + return [ + { + key: 'alias', + header: 'Alias', + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '150px', + cell: (row) =>

{notationToString(row.price)}

, + }, + { + key: 'quantity', + header: <>Quantity ({firstCurrencyName}), + width: '160px', + cell: (row) =>

{notationToString(row.amount)}

, + }, + { + key: 'total', + header: <>Total ({secondCurrencyName}), + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'time', + header: 'Time', + width: '180px', + cell: (row) =>

{formatTimestamp(row.timestamp)}

, + }, + { + key: 'action', + header: 'Action', + width: '80px', + align: 'left', + cell: (row) => ( + + ), + }, + ]; +} + +export function buildOrderHistoryColumns({ + firstCurrencyName, + secondCurrencyName, + secondAssetUsdPrice, +}: BuildOrderHistoryColumnsArgs): ColumnDef[] { + return [ + { + key: 'pair', + header: 'Pair', + width: '120px', + cell: (row) => ( +

+ {firstCurrencyName}/{secondCurrencyName} +

+ ), + }, + { + key: 'direction', + header: 'Direction', + width: '110px', + cell: (row) => ( +

+ {row.type} +

+ ), + }, + { + key: 'price', + header: <>Price ({secondCurrencyName}), + width: '150px', + cell: (row) =>

{notationToString(row.price)}

, + }, + { + key: 'quantity', + header: <>Quantity ({firstCurrencyName}), + width: '160px', + cell: (row) =>

{notationToString(row.left)}

, + }, + { + key: 'total', + header: <>Total ({secondCurrencyName}), + width: '180px', + cell: (row) => ( + + ), + }, + { + key: 'time', + header: 'Time', + width: '100px', + cell: (row) =>

{formatTimestamp(row.timestamp)}

, + }, + ]; +} diff --git a/src/components/dex/UserOrders/columns/types.ts b/src/components/dex/UserOrders/columns/types.ts new file mode 100644 index 0000000..3f6f104 --- /dev/null +++ b/src/components/dex/UserOrders/columns/types.ts @@ -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; + requestsCountByOrderId: Record; + offersCountByOrderId: Record; + onAfter: () => Promise; +} + +export interface BuildApplyTipsColumnsArgs { + type: 'suitables' | 'offers'; + firstCurrencyName: string; + secondCurrencyName: string; + matrixAddresses: MatrixAddress[]; + secondAssetUsdPrice?: number; + userOrders: OrderRow[]; + pairData: PairData | null; + onAfter: () => Promise; +} + +export interface BuildMyRequestsColumnsArgs { + firstCurrencyName: string; + secondCurrencyName: string; + secondAssetUsdPrice?: number; + matrixAddresses: MatrixAddress[]; + onAfter: () => Promise; +} + +export interface BuildOrderHistoryColumnsArgs { + firstCurrencyName: string; + secondCurrencyName: string; + secondAssetUsdPrice?: number; +} diff --git a/src/components/dex/UserOrders/components/CancelActionCell/index.tsx b/src/components/dex/UserOrders/components/CancelActionCell/index.tsx new file mode 100644 index 0000000..13373d3 --- /dev/null +++ b/src/components/dex/UserOrders/components/CancelActionCell/index.tsx @@ -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 ( + onClick()} + > + {type === 'cancel' ? 'Cancel' : 'Reject'} + + ); +} diff --git a/src/components/dex/UserOrders/components/CancelActionCell/types.ts b/src/components/dex/UserOrders/components/CancelActionCell/types.ts new file mode 100644 index 0000000..bad984a --- /dev/null +++ b/src/components/dex/UserOrders/components/CancelActionCell/types.ts @@ -0,0 +1,5 @@ +export interface CancelActionCellProps { + type?: 'cancel' | 'reject'; + id: string; + onAfter: () => Promise; +} diff --git a/src/components/dex/UserOrders/components/OrderGroupHeader/index.tsx b/src/components/dex/UserOrders/components/OrderGroupHeader/index.tsx new file mode 100644 index 0000000..6ddd560 --- /dev/null +++ b/src/components/dex/UserOrders/components/OrderGroupHeader/index.tsx @@ -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 ( +
+
+

For order

+ +

+ {firstCurrencyName}/{secondCurrencyName} +

+ +

{order.type}

+
+ +
+

Quantity

+ +

+ {notationToString(order.left)} {firstCurrencyName} +

+
+ +
+

Total

+ +

+ {notationToString(order.total)} {secondCurrencyName} +

+
+ +
+

{formatTimestamp(order.timestamp)}

+
+
+ ); +} diff --git a/src/components/dex/UserOrders/components/OrderGroupHeader/styles.module.scss b/src/components/dex/UserOrders/components/OrderGroupHeader/styles.module.scss new file mode 100644 index 0000000..38809d6 --- /dev/null +++ b/src/components/dex/UserOrders/components/OrderGroupHeader/styles.module.scss @@ -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; + } +} diff --git a/src/components/dex/UserOrders/components/OrderGroupHeader/types.ts b/src/components/dex/UserOrders/components/OrderGroupHeader/types.ts new file mode 100644 index 0000000..1ec3b09 --- /dev/null +++ b/src/components/dex/UserOrders/components/OrderGroupHeader/types.ts @@ -0,0 +1,7 @@ +import OrderRow from '@/interfaces/common/OrderRow'; + +export interface OrderGroupHeaderProps { + order?: OrderRow; + firstCurrencyName: string; + secondCurrencyName: string; +} diff --git a/src/components/dex/UserOrders/components/RequestActionCell/index.tsx b/src/components/dex/UserOrders/components/RequestActionCell/index.tsx new file mode 100644 index 0000000..8865b76 --- /dev/null +++ b/src/components/dex/UserOrders/components/RequestActionCell/index.tsx @@ -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 ( + onClick()}> + {type === 'request' ? 'Request' : 'Accept'} + + ); +} diff --git a/src/components/dex/UserOrders/components/RequestActionCell/types.ts b/src/components/dex/UserOrders/components/RequestActionCell/types.ts new file mode 100644 index 0000000..76d9f46 --- /dev/null +++ b/src/components/dex/UserOrders/components/RequestActionCell/types.ts @@ -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; + connectedOrder?: OrderRow; + userOrders?: OrderRow[]; +} diff --git a/src/components/dex/UserOrders/index.tsx b/src/components/dex/UserOrders/index.tsx new file mode 100644 index 0000000..61e56df --- /dev/null +++ b/src/components/dex/UserOrders/index.tsx @@ -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([]); + const [ordersHistory, setOrdersHistory] = useState([]); + + 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({ + getPrice: (e) => e.price, + getSide: (e) => e.type, + }); + + const renderOrders = () => { + switch (ordersType.type) { + case 'opened': + return !isSm ? ( + r.id} + emptyMessage="No orders" + /> + ) : ( + + ); + case 'matches': + return !isSm ? ( + r.id} + emptyMessage="No suitables" + groupBy={(r) => r.connected_order_id} + renderGroupHeader={({ groupKey }) => ( + String(o.id) === String(groupKey))} + firstCurrencyName={firstCurrencyName} + secondCurrencyName={secondCurrencyName} + /> + )} + /> + ) : ( + + ); + case 'requests': + return !isSm ? ( + r.id} + emptyMessage="No requests" + groupBy={(r) => (r.creator === 'sell' ? r.sell_order_id : r.buy_order_id)} + renderGroupHeader={({ groupKey }) => ( + String(o.id) === String(groupKey))} + firstCurrencyName={firstCurrencyName} + secondCurrencyName={secondCurrencyName} + /> + )} + /> + ) : ( + + ); + case 'offers': + return !isSm ? ( + r.id} + emptyMessage="No offers" + groupBy={(r) => r.connected_order_id} + renderGroupHeader={({ groupKey }) => ( + String(o.id) === String(groupKey))} + firstCurrencyName={firstCurrencyName} + secondCurrencyName={secondCurrencyName} + /> + )} + /> + ) : ( + + ); + case 'history': + return !isSm ? ( + r.id} + emptyMessage="No data" + /> + ) : ( + + ); + default: + return null; + } + }; + + return ( + <> +
+
+ setActiveTab(t.type)} + /> + + {ordersType?.type === 'opened' && userOrders.length > 0 && ( + + Cancel all + + )} +
+ + {!myOrdersLoading && loggedIn && renderOrders()} + + {myOrdersLoading && loggedIn && } + {!loggedIn && } +
+ + {alertState && ( + setAlertState(null)} + /> + )} + + ); +}; + +export default UserOrders; diff --git a/src/components/dex/UserOrders/styles.module.scss b/src/components/dex/UserOrders/styles.module.scss new file mode 100644 index 0000000..122569f --- /dev/null +++ b/src/components/dex/UserOrders/styles.module.scss @@ -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; + } + } +} diff --git a/src/components/dex/UserOrders/types.ts b/src/components/dex/UserOrders/types.ts new file mode 100644 index 0000000..cd10ded --- /dev/null +++ b/src/components/dex/UserOrders/types.ts @@ -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; + userOrders: OrderRow[]; + applyTips: ApplyTip[]; + myOrdersLoading: boolean; + handleCancelAllOrders: () => void; + secondAssetUsdPrice: number | undefined; + matrixAddresses: MatrixAddress[]; + pairData: PairData | null; + onAfter: () => Promise; +} diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..1e4cc39 --- /dev/null +++ b/src/constants/index.ts @@ -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', + }, +]; diff --git a/src/hook/useAlert.ts b/src/hook/useAlert.ts new file mode 100644 index 0000000..dd282cc --- /dev/null +++ b/src/hook/useAlert.ts @@ -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, + }; +}; diff --git a/src/hook/useFilteredData.ts b/src/hook/useFilteredData.ts new file mode 100644 index 0000000..e477fb3 --- /dev/null +++ b/src/hook/useFilteredData.ts @@ -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; diff --git a/src/hook/useMatrixAddresses.ts b/src/hook/useMatrixAddresses.ts new file mode 100644 index 0000000..eb03242 --- /dev/null +++ b/src/hook/useMatrixAddresses.ts @@ -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([]); + + 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; diff --git a/src/hook/useMediaQuery.ts b/src/hook/useMediaQuery.ts new file mode 100644 index 0000000..8efe98f --- /dev/null +++ b/src/hook/useMediaQuery.ts @@ -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; +} diff --git a/src/hook/useMouseLeave.ts b/src/hook/useMouseLeave.ts new file mode 100644 index 0000000..dc284ec --- /dev/null +++ b/src/hook/useMouseLeave.ts @@ -0,0 +1,21 @@ +import { ForwardedRef, useEffect } from 'react'; + +const useMouseLeave = (ref: ForwardedRef, 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; diff --git a/src/hook/useOrdereForm.ts b/src/hook/useOrdereForm.ts new file mode 100644 index 0000000..774b05d --- /dev/null +++ b/src/hook/useOrdereForm.ts @@ -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; +} + +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(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, + }; +} diff --git a/src/hook/useQuerySyncedTab.ts b/src/hook/useQuerySyncedTab.ts new file mode 100644 index 0000000..d24d0dc --- /dev/null +++ b/src/hook/useQuerySyncedTab.ts @@ -0,0 +1,71 @@ +import { useEffect, useMemo, useState } from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +type TabType = string; + +type TabItem = { + title: string; + type: T; + length?: number; +}; + +type Options = { + tabs: TabItem[]; + queryKey?: string; + defaultType?: T; + replace?: boolean; +}; + +export function useQuerySyncedTab({ + tabs, + queryKey = 'tab', + defaultType, + replace = true, +}: Options) { + 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>(initialTab); + + useEffect(() => { + setActive(initialTab); + }, [initialTab]); + + const setActiveTab = (next: TabItem | 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 }; +} diff --git a/src/hook/useScroll.ts b/src/hook/useScroll.ts new file mode 100644 index 0000000..baf2f2c --- /dev/null +++ b/src/hook/useScroll.ts @@ -0,0 +1,20 @@ +import { useCallback, useRef } from 'react'; + +function useScroll() { + const elementRef = useRef(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; diff --git a/src/hook/useSocketListeners.ts b/src/hook/useSocketListeners.ts new file mode 100644 index 0000000..8ee0514 --- /dev/null +++ b/src/hook/useSocketListeners.ts @@ -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>; + setApplyTips: Dispatch>; + setPairStats: Dispatch>; + setOrdersHistory: Dispatch>; + ordersHistory: PageOrderData[]; + updateOrders: () => Promise; +} + +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'); + }; + }, []); +}; diff --git a/src/hook/useTradeInit.ts b/src/hook/useTradeInit.ts new file mode 100644 index 0000000..d718de8 --- /dev/null +++ b/src/hook/useTradeInit.ts @@ -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; diff --git a/src/hook/useTradingData.ts b/src/hook/useTradingData.ts new file mode 100644 index 0000000..0c0ec6c --- /dev/null +++ b/src/hook/useTradingData.ts @@ -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>; + setOrdersHistory: Dispatch>; + setTrades: Dispatch>; + setPairData: Dispatch>; + setPairStats: Dispatch>; + setUserOrders: Dispatch>; + setApplyTips: Dispatch>; + setMyOrdersLoading: Dispatch>; +} + +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, + }; +} diff --git a/src/interfaces/common/ContextValue.ts b/src/interfaces/common/ContextValue.ts index 43ed69e..4ca5496 100644 --- a/src/interfaces/common/ContextValue.ts +++ b/src/interfaces/common/ContextValue.ts @@ -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 { diff --git a/src/interfaces/common/MatrixAddress.ts b/src/interfaces/common/MatrixAddress.ts new file mode 100644 index 0000000..23f6451 --- /dev/null +++ b/src/interfaces/common/MatrixAddress.ts @@ -0,0 +1,6 @@ +interface MatrixAddress { + address: string; + registered: boolean; +} + +export default MatrixAddress; diff --git a/src/interfaces/common/Period.ts b/src/interfaces/common/Period.ts index 3aa1672..87b8b74 100644 --- a/src/interfaces/common/Period.ts +++ b/src/interfaces/common/Period.ts @@ -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; diff --git a/src/interfaces/common/UserPendingType.ts b/src/interfaces/common/UserPendingType.ts new file mode 100644 index 0000000..f8b8690 --- /dev/null +++ b/src/interfaces/common/UserPendingType.ts @@ -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; diff --git a/src/interfaces/common/orderFormOutput.ts b/src/interfaces/common/orderFormOutput.ts new file mode 100644 index 0000000..e31124a --- /dev/null +++ b/src/interfaces/common/orderFormOutput.ts @@ -0,0 +1,24 @@ +import { Dispatch, SetStateAction } from 'react'; + +interface OrderFormOutput { + price: string; + amount: string; + total: string; + priceValid: boolean; + amountValid: boolean; + totalValid: boolean; + totalUsd: string | undefined; + rangeInputValue: string; + setRangeInputValue: Dispatch>; + onPriceChange: (_inputValue: string) => void; + onAmountChange: (_inputValue: string) => void; + resetForm: () => void; + setTotal: Dispatch>; + setPrice: Dispatch>; + setAmount: Dispatch>; + setPriceValid: Dispatch>; + setAmountValid: Dispatch>; + setTotalValid: Dispatch>; +} + +export default OrderFormOutput; diff --git a/src/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps.ts b/src/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps.ts index 23ae30a..a6e8d35 100644 --- a/src/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps.ts +++ b/src/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps.ts @@ -8,6 +8,7 @@ interface HorizontalSelectProps { setValue: Dispatch>; className?: string; isTab?: boolean; + isSm?: boolean; } export default HorizontalSelectProps; diff --git a/src/interfaces/props/components/default/Header/NavBar/NavBarProps.ts b/src/interfaces/props/components/default/Header/NavBar/NavBarProps.ts index 10bebf8..4ce70d8 100644 --- a/src/interfaces/props/components/default/Header/NavBar/NavBarProps.ts +++ b/src/interfaces/props/components/default/Header/NavBar/NavBarProps.ts @@ -1,5 +1,6 @@ interface NavBarProps { mobile?: boolean; + isLg?: boolean; } export default NavBarProps; diff --git a/src/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps.ts b/src/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps.ts index 167f518..9df9fd2 100644 --- a/src/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps.ts +++ b/src/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps.ts @@ -4,6 +4,10 @@ import Period from '@/interfaces/common/Period'; interface CandleChartProps { candles: CandleRow[]; period: Period; + currencyNames: { + firstCurrencyName: string; + secondCurrencyName: string; + }; } export default CandleChartProps; diff --git a/src/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps.ts b/src/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps.ts index 1173cbd..8eefef6 100644 --- a/src/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps.ts +++ b/src/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps.ts @@ -1,29 +1,28 @@ -import AlertType from '@/interfaces/common/AlertType'; import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue'; import { Dispatch, SetStateAction } from 'react'; interface InputPanelItemProps { + currencyNames: { + firstCurrencyName: string; + secondCurrencyName: string; + }; priceState: string; amountState: string; totalState: string; - buySellValues: SelectValue[]; buySellState: SelectValue; setBuySellState: Dispatch>; setPriceFunction: (_value: string) => void; setAmountFunction: (_value: string) => void; - setAlertState: Dispatch>; - setAlertSubtitle: Dispatch>; setRangeInputValue: Dispatch>; rangeInputValue: string; - firstCurrencyName: string; - secondCurrencyName: string; balance: number | undefined; + zanoBalance: number | undefined; amountValid: boolean; priceValid: boolean; totalValid: boolean; totalUsd: string | undefined; scrollToOrderList: () => void; - updateUserOrders: () => void; + onAfter: () => Promise; } export default InputPanelItemProps; diff --git a/src/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps.ts b/src/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps.ts index e506c47..3a3df12 100644 --- a/src/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps.ts +++ b/src/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps.ts @@ -2,9 +2,7 @@ interface LabeledInputProps { label: string; value: string; setValue: (_value: string) => void; - placeholder: string; currency: string; - usd?: string; readonly?: boolean; invalid?: boolean; } diff --git a/src/interfaces/props/pages/dex/trading/StatItemProps.ts b/src/interfaces/props/pages/dex/trading/StatItemProps.ts index cf99aeb..20f0ec3 100644 --- a/src/interfaces/props/pages/dex/trading/StatItemProps.ts +++ b/src/interfaces/props/pages/dex/trading/StatItemProps.ts @@ -5,6 +5,7 @@ interface StatItemProps { title: string; value: string; coefficient?: number; + className?: string; } export default StatItemProps; diff --git a/src/interfaces/responses/trades/GetTradeRes.ts b/src/interfaces/responses/trades/GetTradeRes.ts new file mode 100644 index 0000000..c3d898f --- /dev/null +++ b/src/interfaces/responses/trades/GetTradeRes.ts @@ -0,0 +1,15 @@ +export interface Trade { + id: number; + timestamp: number; + amount: string; + price: string; + type: string; + buyer: { + address: string; + amount: string; + }; + seller: { + address: string; + amount: string; + }; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index bce9305..dc7bb30 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,5 +1,6 @@ import '@/styles/globals.scss'; import '@/styles/themes/light.scss'; +import '@/styles/themes/dark.scss'; import Head from 'next/head'; import { StoreProvider } from '@/store/store-reducer'; import NextApp, { AppContext, AppProps } from 'next/app'; diff --git a/src/pages/dex/pairs/PairsCard/PairsCard.tsx b/src/pages/dex/pairs/PairsCard/PairsCard.tsx index c0b5c2d..9e92456 100644 --- a/src/pages/dex/pairs/PairsCard/PairsCard.tsx +++ b/src/pages/dex/pairs/PairsCard/PairsCard.tsx @@ -22,7 +22,7 @@ export default function PairsCard({ pair }: IProps) { const secondAssetUsdPrice = state.assetsRates.get(secondCurrency.asset_id || '') ?? 0; - const price = Number(roundTo(notationToString(pair.rate ?? 0), 2)); + const price = Number(roundTo(notationToString(pair.rate ?? 0), 4)); const currentPriceUSD = secondAssetUsdPrice ? price : 0; const priceUSD = currentPriceUSD ? String(`$${(secondAssetUsdPrice * price).toFixed(2)}`) @@ -34,7 +34,7 @@ export default function PairsCard({ pair }: IProps) { ? -99.99 : parseFloat(coefficient?.toFixed(2) || '0'); - const volume = Number(roundTo(notationToString(pair.volume ?? 0), 2)); + const volume = Number(roundTo(notationToString(pair.volume ?? 0), 4)); const currentVolumeUSD = secondAssetUsdPrice ? volume : 0; const volumeUSD = currentVolumeUSD ? String(`$${(secondAssetUsdPrice * volume).toFixed(2)}`) diff --git a/src/pages/dex/trading/CandleChart/CandleChart.module.scss b/src/pages/dex/trading/CandleChart/CandleChart.module.scss deleted file mode 100644 index 447d07f..0000000 --- a/src/pages/dex/trading/CandleChart/CandleChart.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -.candle__chart__wrapper { - position: relative; - width: auto; - height: auto; - height: 100%; - - height: 515px; - - > canvas { - width: 100%; - height: 100%; - cursor: crosshair; - } - - h1 { - 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; - } - } -} diff --git a/src/pages/dex/trading/CandleChart/CandleChart.tsx b/src/pages/dex/trading/CandleChart/CandleChart.tsx deleted file mode 100644 index fe6684e..0000000 --- a/src/pages/dex/trading/CandleChart/CandleChart.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import { useEffect, useState, useRef, useMemo } from 'react'; -import useAdvancedTheme from '@/hook/useTheme'; -import CandleChartProps from '@/interfaces/props/pages/dex/trading/CandleChartProps/CandleChartProps'; -import ReactECharts from 'echarts-for-react'; -import Decimal from 'decimal.js'; -import * as echarts from 'echarts'; -import CandleRow from '@/interfaces/common/CandleRow'; -import testCandles from './testCandles.json'; -import styles from './CandleChart.module.scss'; - -const TESTING_MODE = false; - -function CandleChart(props: CandleChartProps) { - type ResultCandle = [number, number, number, number, number]; - - const { theme } = useAdvancedTheme(); - const chartRef = useRef(null); - const upColor = '#16D1D6cc'; - const upBorderColor = '#16D1D6cc'; - const downColor = '#ff6767cc'; - const downBorderColor = '#ff6767cc'; - - const [candles, setCandles] = useState([]); - const [isLoaded, setIsLoaded] = useState(false); - - function timestampToString(timestamp: string) { - const targetPattern = - new Date(parseInt(timestamp, 10)).toDateString() === new Date().toDateString() - ? 'hh:mm:ss' - : 'hh:mm:ss\ndd-MM-yyyy'; - - return echarts.format.formatTime(targetPattern, parseInt(timestamp, 10)); - } - - function prepareCandles() { - const candles = (TESTING_MODE ? (testCandles as CandleRow[]) : props.candles) - .map((candle) => { - const result = [ - parseInt(candle.timestamp, 10), - candle.shadow_top || 0, - candle.shadow_bottom || 0, - candle.body_first || 0, - candle.body_second || 0, - ]; - - return result as ResultCandle; - }) - .filter((e) => e[0]) - .map((e) => { - const decimals = e.map((el, i) => ({ - value: i !== 0 ? new Decimal(el) : undefined, - index: i, - })); - - for (const decimal of decimals) { - if (decimal.value !== undefined) { - if (decimal.value.lessThan(0.00001)) { - e[decimal.index] = 0; - } - } - } - - return e; - }); - - return candles; - } - - useEffect(() => { - const newCandles = prepareCandles(); - - setCandles(newCandles); - setIsLoaded(true); - }, [props.candles]); - - const option = useMemo(() => { - function splitData(rawData: ResultCandle[]) { - const categoryData = []; - const values = []; - for (let i = 0; i < rawData.length; i++) { - categoryData.push(rawData[i][0]); - values.push(rawData[i].slice(1)); - } - return { - categoryData, - values, - }; - } - - const data0 = splitData(candles); - - const now = new Date().getTime(); - const zoomStartTime = (() => { - const date = +new Date(); - const hr1 = 60 * 60 * 1000; - - const hoursDecrement = 24 * hr1; - const daysDecrement = 7 * 24 * hr1; - const weeksDecrement = 4 * 7 * 24 * hr1; - const monthsDecrement = 52 * 7 * 24 * hr1; - - switch (props.period) { - case '1h': - return date - hoursDecrement; - case '1d': - return date - daysDecrement; - case '1w': - return date - weeksDecrement; - case '1m': - return date - monthsDecrement; - default: - return date - hr1; - } - })(); - - const closestDateToStart = data0.categoryData.reduce((acc, curr) => { - const currDiff = Math.abs(curr - zoomStartTime); - const accDiff = Math.abs(acc - zoomStartTime); - - return currDiff < accDiff ? curr : acc; - }, now); - - const lastDate = data0.categoryData[data0.categoryData.length - 1]; - - const closestDateIndex = data0.categoryData.indexOf(closestDateToStart); - const lastDateIndex = data0.categoryData.indexOf(lastDate); - - return { - grid: { - top: '5%', - left: '10%', - right: '5%', - bottom: '10%', - }, - - xAxis: { - type: 'category', - // data: data0.categoryData, - boundaryGap: true, - axisLine: { onZero: false }, - splitLine: { - show: true, - lineStyle: { - color: theme === 'light' ? '#e3e3e8' : '#1f1f4a', - }, - }, - min: 'dataMin', - max: 'dataMax', - axisPointer: { - show: true, - type: 'line', - label: { - formatter: (params: { value: string }) => timestampToString(params.value), - backgroundColor: '#4A90E2', - color: '#ffffff', - }, - }, - axisLabel: { - formatter: timestampToString, - }, - }, - yAxis: { - scale: true, - splitArea: { show: false }, - min: 0, - max: (value: { max: string }) => new Decimal(value.max).mul(1.1).toNumber(), - axisPointer: { - show: true, - type: 'line', - label: { - show: true, - backgroundColor: '#4A90E2', - color: '#ffffff', - }, - }, - splitLine: { - show: true, - lineStyle: { - color: theme === 'light' ? '#e3e3e8' : '#1f1f4a', - }, - }, - }, - dataZoom: [ - { - type: 'inside', - startValue: closestDateIndex, - endValue: lastDateIndex, - }, - ], - series: [ - { - name: 'Candle Chart', - type: 'candlestick', - data: candles, - itemStyle: { - color: upColor, - color0: downColor, - borderColor: upBorderColor, - borderColor0: downBorderColor, - }, - barWidth: '75%', - dimensions: ['date', 'highest', 'lowest', 'open', 'close'], - encode: { - x: 'date', - y: ['open', 'close', 'highest', 'lowest'], - }, - large: true, - largeThreshold: 2000000, - }, - ], - }; - }, [candles, theme]); - - console.log('option', option); - - return ( -
- - - {!candles?.length && isLoaded &&

[ Low volume ]

} -
- ); -} - -export default CandleChart; diff --git a/src/pages/dex/trading/InputPanelItem/InputPanelItem.module.scss b/src/pages/dex/trading/InputPanelItem/InputPanelItem.module.scss deleted file mode 100644 index d79f64d..0000000 --- a/src/pages/dex/trading/InputPanelItem/InputPanelItem.module.scss +++ /dev/null @@ -1,218 +0,0 @@ -.input_panel__item { - width: 100%; - - > div:first-child { - display: flex; - justify-content: space-between; - padding-bottom: 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - margin-bottom: 20px; - } - - > div:last-child { - display: flex; - flex-direction: column; - gap: 20px; - - > .buy_btn { - background-color: #16d1d6; - - &:hover { - background-color: #45dade; - } - } - - > .sell_btn { - background-color: #ff6767; - - &:hover { - background-color: #ff8585; - } - } - - .input_panel__range { - margin-top: 10px; - } - - .input_panel__expiration { - display: flex; - justify-content: space-between; - gap: 20px; - - h6 { - white-space: nowrap; - } - - .expiration__dropdown { - width: 100%; - } - - > div:first-child { - display: flex; - align-items: center; - gap: 6px; - } - - @media screen and (max-width: 1500px) { - flex-wrap: wrap; - } - - @media screen and (max-width: 1000px) { - flex-wrap: nowrap; - } - - @media screen and (max-width: 530px) { - flex-wrap: wrap; - } - } - - .labeled_input { - display: flex; - flex-direction: column; - gap: 8px; - - h6 { - color: var(--font-dimmed-color); - } - - > div { - width: 100%; - position: relative; - background-color: var(--bordered-input-bg); - border: 1px solid var(--window-border-color); - border-radius: 8px; - display: flex; - overflow: hidden; - - input { - width: 100%; - padding: 16px 15px; - background-color: transparent; - border: none; - } - - .labeled_input__value { - padding-right: 15px; - display: flex; - align-items: center; - - > p { - color: var(--font-dimmed-color); - } - } - - .labeled_input__currency { - min-width: 82px; - max-width: 150px; - padding: 0 15px; - background-color: var(--dex-input-currency); - display: flex; - align-items: center; - justify-content: center; - - > p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - &.labeled_input__invalid > div { - border-color: #ff6767; - } - - @media screen and (max-width: 430px) { - > div { - input, - .labeled_input__value > p, - .labeled_input__currency > p { - font-size: 13px; - } - - input { - padding: 19px 15px; - } - - .labeled_input__currency { - min-width: 70px; - } - } - } - } - - .input_panel__fees { - display: flex; - justify-content: space-between; - - p { - color: var(--font-dimmed-color); - } - } - } -} - -.buy-sell-switch { - padding: 3px; - height: 30px; - display: flex; - align-items: center; - border-radius: 100px; - border: 1px solid var(--dex-buy-sell-border); - - .buy-sell-switch__item { - width: 50px; - height: 100%; - background-color: transparent; - cursor: pointer; - font-size: 14px; - font-weight: 600; - border-radius: 100px; - - &.item_selected-buy { - background-color: #16d1d6; - color: #ffffff; - } - - &.item_selected-sell { - background-color: #ff6767; - color: #ffffff; - } - } -} - -.apply__alert { - display: flex; - gap: 20px; - align-items: center; - - &__content { - display: flex; - flex-direction: column; - gap: 10px; - } - - &__button { - max-width: 125px; - background-color: var(--alert-btn-bg); - color: #1f8feb; - padding: 7px 32px; - font-size: 12px; - font-weight: 500; - - &:hover { - background-color: var(--alert-btn-hover); - } - } - - h2 { - font-size: 16px; - font-weight: 600; - } - - p { - font-size: 14px; - opacity: 0.7; - margin-bottom: 5px; - } -} diff --git a/src/pages/dex/trading/InputPanelItem/InputPanelItem.tsx b/src/pages/dex/trading/InputPanelItem/InputPanelItem.tsx deleted file mode 100644 index 2d721e3..0000000 --- a/src/pages/dex/trading/InputPanelItem/InputPanelItem.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { Store } from '@/store/store-reducer'; -import { createOrder } from '@/utils/methods'; -import { ChangeEvent, useContext, useRef, useState } from 'react'; -import Input from '@/components/UI/Input/Input'; -import RangeInput from '@/components/UI/RangeInput/RangeInput'; -import ConnectButton from '@/components/UI/ConnectButton/ConnectButton'; -import Button from '@/components/UI/Button/Button'; -import { useRouter } from 'next/router'; -import { classes, formatDollarValue } from '@/utils/utils'; -import InputPanelItemProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/InputPanelItemProps'; -import LabeledInputProps from '@/interfaces/props/pages/dex/trading/InputPanelItem/LabeledInputProps'; -import CreateOrderData from '@/interfaces/fetch-data/create-order/CreateOrderData'; -import Decimal from 'decimal.js'; -import HorizontalSelectProps from '@/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps'; -import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue'; -import { nanoid } from 'nanoid'; -import Alert from '@/components/UI/Alert/Alert'; -import infoIcon from '@/assets/images/UI/info_alert_icon.svg'; -import Image from 'next/image'; -import styles from './InputPanelItem.module.scss'; - -function DexBuySellSwitch({ body, value, setValue }: HorizontalSelectProps) { - return ( -
- {body.map((e) => { - let itemClass = styles['buy-sell-switch__item']; - - if (value.code === e.code) { - itemClass += - e.code === 'buy' - ? ` ${styles['item_selected-buy']}` - : ` ${styles['item_selected-sell']}`; - } - - return ( - - ); - })} -
- ); -} - -function InputPanelItem(props: InputPanelItemProps) { - const { state } = useContext(Store); - - const router = useRouter(); - - const { - priceState = '', - amountState = '', - totalState = '', - buySellValues, - buySellState = buySellValues[0], - setBuySellState, - setPriceFunction, - setAmountFunction, - setAlertState, - setAlertSubtitle, - setRangeInputValue, - rangeInputValue = '50', - firstCurrencyName = '', - secondCurrencyName = '', - balance = 0, - amountValid, - priceValid, - totalValid, - totalUsd, - scrollToOrderList, - updateUserOrders, - } = props; - - const [creatingState, setCreatingState] = useState(false); - - const [hasImmediateMatch, setHasImmediateMatch] = useState(false); - - function LabeledInput(props: LabeledInputProps) { - const labelRef = useRef(null); - const { - label = '', - placeholder = '', - currency = '', - value, - readonly, - usd, - setValue, - invalid, - } = props; - - const handleInput = (e: React.FormEvent) => { - if (!readonly && setValue) { - setValue(e.currentTarget.value); - } - }; - - return ( -
-
{label}
-
- - {usd && ( -
-

~${formatDollarValue(usd)}

-
- )} -
-

{currency}

-
-
-
- ); - } - - const isBuy = buySellState?.code === 'buy'; - - async function postOrder() { - const price = new Decimal(priceState); - const amount = new Decimal(amountState); - - const isFull = - price.greaterThan(0) && - price.lessThan(1000000000) && - amount.greaterThan(0) && - amount.lessThan(1000000000); - - if (!isFull) 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); - } - } else { - setAlertState('error'); - if (result.data === 'Same order') { - setAlertSubtitle('Order already exists'); - } else { - setAlertSubtitle('Failed to create order'); - } - - setTimeout(() => { - setAlertState(null); - setAlertSubtitle(''); - }, 3000); - } - - updateUserOrders(); - } - - function onRangeInput(e: ChangeEvent) { - setRangeInputValue(e.target.value); - if (balance) { - const rangeValue = new Decimal(e.target.value || '0'); - const balanceDecimal = new Decimal(balance || '0'); - const calculatedAmount = balanceDecimal.mul(rangeValue.div(100)).toString(); - setAmountFunction(calculatedAmount || ''); - } - } - - let buttonText; - - if (creatingState) { - buttonText = 'Creating...'; - } else if (isBuy) { - buttonText = 'Buy'; - } else { - buttonText = 'Sell'; - } - - return ( -
- {hasImmediateMatch && ( - - success -
-

Apply the order

-

You have to apply the order

- -
-
- } - close={() => setHasImmediateMatch(false)} - /> - )} - -
-
New order
- -
- -
- {LabeledInput({ - value: priceState, - setValue: setPriceFunction, - currency: secondCurrencyName, - placeholder: '0.00', - label: 'Price', - invalid: !!priceState && !priceValid, - })} - {LabeledInput({ - value: amountState, - setValue: setAmountFunction, - currency: firstCurrencyName, - placeholder: '0.00', - label: 'Amount', - invalid: !!amountState && !amountValid, - })} - - {LabeledInput({ - value: totalState, - setValue: () => undefined, - currency: secondCurrencyName, - placeholder: '0.00', - label: 'Total', - readonly: true, - invalid: !!totalState && !totalValid, - usd: totalUsd, - })} - {state.wallet?.connected ? ( - - ) : ( - - )} -
-

- Fee: 0.01 Zano -

-
-
-
- ); -} - -export default InputPanelItem; diff --git a/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.module.scss b/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.module.scss deleted file mode 100644 index aac0fd1..0000000 --- a/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.module.scss +++ /dev/null @@ -1,31 +0,0 @@ -.orders-buy-sell-switch { - display: flex; - align-items: center; - gap: 8px; - background-color: transparent !important; - cursor: pointer; - - &:hover { - opacity: 0.7; - } - - > p { - font-size: 16px; - font-weight: 600; - color: #16d1d6; - } - - > svg > * { - fill: transparent; - } - - &.orders-buy-sell-switch_sell { - > p { - color: #ff6767; - } - - > svg > * { - stroke: #ff6767; - } - } -} diff --git a/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.tsx b/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.tsx deleted file mode 100644 index fbe8d93..0000000 --- a/src/pages/dex/trading/OrdersBuySellSwitch/OrdersBuySellSwitch.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import HorizontalSelectProps from '@/interfaces/props/components/UI/HorizontalSelect/HorizontalSelectProps'; -import { ReactComponent as ArrowIcon } from '@/assets/images/UI/trade_arrow.svg'; -import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue'; -import { classes } from '@/utils/utils'; -import styles from './OrdersBuySellSwitch.module.scss'; - -export default function OrdersBuySellSwitch({ - body, - value, - setValue, - className, -}: HorizontalSelectProps) { - const defaultValue = body[0]; - - const buyValue = body.find((e) => e.code === 'buy'); - const sellValue = body.find((e) => e.code === 'sell'); - - const isBuy = value.code === 'buy'; - - return ( - - ); -} diff --git a/src/pages/dex/trading/[id].tsx b/src/pages/dex/trading/[id].tsx index ea28999..117c1e5 100644 --- a/src/pages/dex/trading/[id].tsx +++ b/src/pages/dex/trading/[id].tsx @@ -1,45 +1,11 @@ import styles from '@/styles/Trading.module.scss'; import Footer from '@/components/default/Footer/Footer'; import Header from '@/components/default/Header/Header'; -import PageTitle from '@/components/default/PageTitle/PageTitle'; -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 { ReactComponent as NoOffersIcon } from '@/assets/images/UI/no_offers.svg'; -import { ReactComponent as ArrowRight } from '@/assets/images/UI/arrow-outlined-right.svg'; -import Dropdown from '@/components/UI/Dropdown/Dropdown'; import HorizontalSelect from '@/components/UI/HorizontalSelect/HorizontalSelect'; -import { useContext, useEffect, useRef, useState } from 'react'; -import { Store } from '@/store/store-reducer'; -import { useRouter } from 'next/router'; -import { - applyOrder, - cancelOrder, - getOrdersPage, - confirmTransaction, - getPair, - getPairStats, - getUserOrdersPage, - getCandles, -} from '@/utils/methods'; +import { useCallback, useState } from 'react'; +import { cancelOrder } from '@/utils/methods'; import ContentPreloader from '@/components/UI/ContentPreloader/ContentPreloader'; -import Link from 'next/link'; -import { nanoid } from 'nanoid'; -import { - cutAddress, - formatDollarValue, - getAssetIcon, - isPositiveFloatStr, - notationToString, - roundTo, - shortenAddress, - tradingKnownCurrencies, -} from '@/utils/utils'; import Alert from '@/components/UI/Alert/Alert'; -import socket from '@/utils/socket'; -import SelectValue from '@/interfaces/states/pages/dex/trading/InputPanelItem/SelectValue'; -import AlertType from '@/interfaces/common/AlertType'; import PeriodState from '@/interfaces/states/pages/dex/trading/InputPanelItem/PeriodState'; import OrderRow from '@/interfaces/common/OrderRow'; import ApplyTip from '@/interfaces/common/ApplyTip'; @@ -47,1355 +13,219 @@ import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes'; import { PairStats } from '@/interfaces/responses/orders/GetPairStatsRes'; import PairData from '@/interfaces/common/PairData'; import CandleRow from '@/interfaces/common/CandleRow'; -import StatItemProps from '@/interfaces/props/pages/dex/trading/StatItemProps'; -import { confirmIonicSwap, ionicSwap } from '@/utils/wallet'; -import Decimal from 'decimal.js'; -import Tooltip from '@/components/UI/Tooltip/Tooltip'; -import { updateAutoClosedNotification } from '@/store/actions'; +import { Trade } from '@/interfaces/responses/trades/GetTradeRes'; +import { periods, buySellValues } from '@/constants'; +import { useAlert } from '@/hook/useAlert'; +import useScroll from '@/hook/useScroll'; +import InputPanelItem from '@/components/dex/InputPanelItem'; +import TradingHeader from '@/components/dex/TradingHeader'; +import UserOrders from '@/components/dex/UserOrders'; +import OrdersPool from '@/components/dex/OrdersPool'; +import CandleChart from '@/components/dex/CandleChart'; +import { useSocketListeners } from '@/hook/useSocketListeners'; +import { useTradingData } from '@/hook/useTradingData'; +import useFilteredData from '@/hook/useFilteredData'; +import useTradeInit from '@/hook/useTradeInit'; +import useMatrixAddresses from '@/hook/useMatrixAddresses'; +import takeOrderClick from '@/utils/takeOrderClick'; import useUpdateUser from '@/hook/useUpdateUser'; -import LightningImg from '@/assets/images/UI/lightning.png'; -import RocketImg from '@/assets/images/UI/rocket.png'; -import { ReactComponent as ConnectionIcon } from '@/assets/images/UI/connection.svg'; -import Image from 'next/image'; -import CandleChart from './CandleChart/CandleChart'; -import OrdersBuySellSwitch from './OrdersBuySellSwitch/OrdersBuySellSwitch'; -import InputPanelItem from './InputPanelItem/InputPanelItem'; -import { validateTokensInput } from '../../../../shared/utils'; - -function BadgeStatus({ type = 'instant' }: { type?: 'instant' | 'high' }) { - return ( -
- badge image - {type === 'instant' ? 'instant' : 'high volume'} -
- ); -} function Trading() { - const router = useRouter(); + const { alertState, alertSubtitle, setAlertState } = useAlert(); + const { elementRef: orderListRef, scrollToElement: scrollToOrdersList } = + useScroll(); + const { elementRef: orderFormRef, scrollToElement: scrollToOrderForm } = + useScroll(); + const fetchUser = useUpdateUser(); - const [ordersHistory, setOrdersHistory] = useState([]); - - const orderFormRef = useRef(null); - const orderListRef = useRef(null); - - const pairId = typeof router.query.id === 'string' ? router.query.id : ''; - - const periods: PeriodState[] = [ - { - name: '1H', - code: '1h', - }, - { - name: '1D', - code: '1d', - }, - { - name: '1W', - code: '1w', - }, - { - name: '1M', - code: '1m', - }, - ]; - - const buySellValues: SelectValue[] = [ - { - name: 'Buy', - code: 'buy', - }, - { - name: 'Sell', - code: 'sell', - }, - ]; - - const { state, dispatch } = useContext(Store); - const [userOrders, setUserOrders] = useState([]); - - const [periodsState, setPeriodsState] = useState(periods[0]); - + const [periodsState, setPeriodsState] = useState(periods[4]); const [pairData, setPairData] = useState(null); - const [candles, setCandles] = useState([]); - - const [candlesLoaded, setCandlesLoaded] = useState(false); - - const [ordersLoading, setOrdersLoading] = useState(true); - + const [trades, setTrades] = useState([]); const [myOrdersLoading, setMyOrdersLoading] = useState(true); - const [ordersBuySell, setOrdersBuySell] = useState(buySellValues[0]); - const [pairStats, setPairStats] = useState(null); - const [applyTips, setApplyTips] = useState([]); + const matrixAddresses = useMatrixAddresses(ordersHistory); + const [orderFormType, setOrderFormType] = useState(buySellValues[1]); - const [alertState, setAlertState] = useState(null); + const { + orderForm, + currencyNames, + firstAssetLink, + secondAssetLink, + secondAssetUsdPrice, + balance, + zanoBalance, + pairRateUsd, + } = useTradeInit({ pairData, pairStats }); - const [alertSubtitle, setAlertSubtitle] = useState(''); + const { + fetchTrades, + updateOrders, + updateUserOrders, + candlesLoaded, + ordersLoading, + tradesLoading, + } = useTradingData({ + periodsState, + setApplyTips, + setCandles, + setMyOrdersLoading, + setOrdersHistory, + setPairData, + setPairStats, + setTrades, + setUserOrders, + }); - const [matrixAddresses, setMatrixAddresses] = useState([]); + useSocketListeners({ + setUserOrders, + ordersHistory, + setApplyTips, + setOrdersHistory, + setPairStats, + updateOrders, + }); - async function updateOrders() { - setOrdersLoading(true); - const result = await getOrdersPage(pairId); - if (!result.success) return; - setOrdersHistory(result?.data || []); - setOrdersLoading(false); - } - - 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'); - }; - }, []); - - // Detect registered addresses - const hasConnection = (address: string) => - matrixAddresses.some( - (item: { address: string; registered: boolean }) => - item.address === address && item.registered, - ); - - const filteredOrdersHistory = ordersHistory - ?.filter((e) => e.type === ordersBuySell.code) - ?.filter((e) => e.user.address !== state.wallet?.address) - ?.sort((a, b) => { - if (ordersBuySell.code === 'buy') { - return parseFloat(b.price.toString()) - parseFloat(a.price.toString()); - } - return parseFloat(a.price.toString()) - parseFloat(b.price.toString()); - }); - - // Get registered addresses from matrix - useEffect(() => { - const fetchConnections = async () => { - const filteredAddresses = ordersHistory?.map((e) => e?.user?.address); - if (!filteredAddresses.length) return; - const response = await fetch('https://messenger.zano.org/api/get-addresses', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - addresses: filteredAddresses, - }), + // Take order from trades + const onHandleTakeOrder = useCallback( + (event: React.MouseEvent, e: PageOrderData) => { + setOrderFormType(() => { + return e.type === 'buy' ? buySellValues[2] : buySellValues[1]; }); - const data = await response.json(); - setMatrixAddresses(data.addresses); - }; + takeOrderClick({ + event, + PageOrderData: e, + balance, + orderForm, + pairData, + scrollToOrderForm, + }); + }, + [balance, orderForm, pairData, scrollToOrderForm], + ); - fetchConnections(); - }, [ordersHistory]); + // Cancel all user orders + const handleCancelAllOrders = useCallback(async () => { + if (!userOrders.length) return; - const firstCurrencyName = pairData?.first_currency?.name || ''; - const secondCurrencyName = pairData?.second_currency?.name || ''; - - 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 secondCurrencyDP = pairData?.second_currency.asset_info?.decimal_point || 12; - - useEffect(() => { - async function fetchPairStats() { - const result = await getPairStats(pairId); - if (!result.success) return; - setPairStats(result.data); - } - - fetchPairStats(); - }, []); - - useEffect(() => { - async function fetchCandles() { - setCandlesLoaded(false); - setCandles([]); - const result = await getCandles(pairId, periodsState.code); - if (result.success) { - setCandles(result.data); - } else { - setCandles([]); - } - setCandlesLoaded(true); - } - - fetchCandles(); - }, [periodsState]); - - useEffect(() => { - async function getPairData() { - const result = await getPair(pairId); - if (!result.success) { - router.push('/404'); - return; - } - setPairData(result.data); - } - - getPairData(); - }, []); - - async function updateUserOrders() { setMyOrdersLoading(true); - const result = await getUserOrdersPage(pairId); - console.log('result getuserorderspage', result); - await fetchUser(); - - if (!result.success) return; - setUserOrders(result?.data?.orders || []); - setApplyTips(result?.data?.applyTips || []); - setMyOrdersLoading(false); - } - - const loggedIn = !!state.wallet?.connected; - - useEffect(() => { - if (!loggedIn) return; - setUserOrders([]); - updateUserOrders(); - }, [state.wallet?.connected && state.wallet?.address]); - - useEffect(() => { - updateOrders(); - }, []); - - // useEffect(() => { - // socket.on - // }, []); - - const [priceState, setPriceState] = useState(''); - const [amountState, setAmountState] = useState(''); - const [totalState, setTotalState] = useState(''); - - const [totalUsd, setTotalUsd] = useState(undefined); - - const [priceValid, setPriceValid] = useState(false); - const [amountValid, setAmountValid] = useState(false); - const [totalValid, setTotalValid] = useState(false); - - const [buySellState, setBuySellState] = useState(buySellValues[0]); - - const [rangeInputValue, setRangeInputValue] = useState('50'); - - useEffect(() => { - let totalDecimal: Decimal | undefined; try { - totalDecimal = new Decimal(totalState); + await Promise.all(userOrders.map((order) => cancelOrder(order.id))); + await updateUserOrders(); } catch (err) { - console.log(err); + console.error(err); + } finally { + setMyOrdersLoading(false); } - - const zanoPrice = state.assetsRates.get(pairData?.second_currency?.asset_id || ''); - - setTotalUsd(zanoPrice && totalDecimal ? totalDecimal.mul(zanoPrice).toFixed(2) : undefined); - }, [totalState, state.assetsRates, pairData?.second_currency?.asset_id]); - - function setPriceFunction(inputValue: string) { - if (inputValue !== '' && !isPositiveFloatStr(inputValue)) { - return; - } - - try { - const value = new Decimal(inputValue || NaN); - - if (value.toString().replace('.', '').length > 18) { - console.log('TOO MANY DECIMALS'); - return; - } - } catch (error) { - console.log(error); - } - - setPriceState(inputValue); - - if (!inputValue) { - setTotalState(''); - setTotalValid(false); - setPriceValid(false); - return; - } - - const valueDecimal = new Decimal(inputValue || NaN); - const amountDecimal = new Decimal(amountState || NaN); - - const validationResult = validateTokensInput(inputValue, secondCurrencyDP); - - if (!validationResult.valid) { - setTotalState(''); - setTotalValid(false); - setPriceValid(false); - return; - } - - setPriceValid(true); - - if (!valueDecimal.isNaN() && !amountDecimal.isNaN() && amountState !== '') { - const totalDecimal = valueDecimal.mul(amountDecimal); - setTotalState(totalDecimal.toString()); - const total = totalDecimal.toFixed(secondCurrencyDP); - - const totalValidationResult = validateTokensInput(total, secondCurrencyDP); - - setTotalValid(totalValidationResult.valid); - } else { - setTotalState(''); - setTotalValid(false); - } - } - - const assets = state.wallet?.connected ? state.wallet?.assets || [] : []; - - const balance = assets.find((e) => e.ticker === firstCurrencyName)?.balance; - - function setAmountFunction(inputValue: string) { - if (inputValue !== '' && !isPositiveFloatStr(inputValue)) { - return; - } - - try { - const value = new Decimal(inputValue || NaN); - - if (value.toString().replace('.', '').length > 18) { - console.log('TOO MANY DECIMALS'); - return; - } - } catch (error) { - console.log(error); - } - - setAmountState(inputValue); - - if (!inputValue) { - setTotalState(''); - setTotalValid(false); - setAmountValid(false); - return; - } - - const value = new Decimal(inputValue || NaN); - const price = new Decimal(priceState || NaN); - - const validationResult = validateTokensInput( - inputValue, - pairData?.first_currency.asset_info?.decimal_point || 12, - ); - console.log(validationResult); - - if (!validationResult.valid) { - setTotalState(''); - setTotalValid(false); - setAmountValid(false); - return; - } - - setAmountValid(true); - - if (balance) setRangeInputValue(value.div(balance).mul(100).toFixed()); - - if (!price.isNaN() && !value.isNaN() && priceState !== '') { - const totalDecimal = value.mul(price); - setTotalState(totalDecimal.toString()); - const total = totalDecimal.toFixed(secondCurrencyDP); - const totalValidationResult = validateTokensInput(total, secondCurrencyDP); - - setTotalValid(totalValidationResult.valid); - } else { - setTotalState(''); - setTotalValid(false); - } - } - - function setCorrespondingOrder(price: number, amount: number) { - const priceDecimal = new Decimal(price || 0); - const amountDecimal = new Decimal(amount || 0); - const totalDecimal = priceDecimal.mul(amountDecimal); - - setPriceFunction(notationToString(priceDecimal.toString()) || ''); - setAmountFunction(notationToString(amountDecimal.toString()) || ''); - setTotalState(notationToString(totalDecimal.toString()) || ''); - - if (balance) { - const balanceDecimal = new Decimal(balance); - - const percentageDecimal = amountDecimal.div(balanceDecimal).mul(100); - setRangeInputValue(percentageDecimal.toFixed() || ''); - } - - const total = priceDecimal.mul(amountDecimal).toFixed(secondCurrencyDP); - - const totalValidationResult = validateTokensInput( - total, - pairData?.second_currency.asset_info?.decimal_point || 12, - ); - - setTotalValid(totalValidationResult.valid); - } - - function StatItem(props: StatItemProps) { - const { Img } = props; - - return ( -
-
- -

{props.title}

-
-
-

{props.value}

- {props.coefficient !== undefined && ( -

= 0 - ? styles.coefficient__green - : styles.coefficient__red - } - > - {props.coefficient >= 0 ? '+' : ''} - {props.coefficient?.toFixed(2)}% -

- )} -
-
- ); - } - - function takeOrderClick( - event: React.MouseEvent, - e: PageOrderData, - ) { - event.preventDefault(); - setCorrespondingOrder(e.price, e.amount); - setBuySellState( - buySellValues.find((e) => e.code !== ordersBuySell.code) || buySellValues[0], - ); - - if (!orderFormRef.current) return; - - orderFormRef.current.scrollIntoView({ behavior: 'smooth' }); - } - - //* * FOR USAGE IN THIS PAGE TABLES ONLY */ - function OrderRowTooltipCell({ - style, - children, - sideText, - sideTextColor, - }: { - style?: React.CSSProperties; - children: string; - sideText?: string; - sideTextColor?: string; - }) { - const [showTooltip, setShowTooltip] = useState(false); - - const tooltipText = `${children}${sideText ? ` ~${sideText}` : ''}`; - - const isLongContent = tooltipText.length > 14; - - return ( - -

setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - {children} - {sideText && ( - - {sideText} - - )} -

- {isLongContent && ( - - {tooltipText} - - )} - - ); - } - - function MatrixConnectionBadge({ - userAdress, - userAlias, - }: { - userAdress?: string; - userAlias?: string; - }) { - const [connectionTooltip, setConnectionTooltip] = useState(false); - return userAdress && hasConnection(userAdress) ? ( - <> - setConnectionTooltip(true)} - onMouseLeave={() => setConnectionTooltip(false)} - style={{ marginTop: '4px', cursor: 'pointer', position: 'relative' }} - > - - - - Matrix connection - - - ) : ( - <> - ); - } - - function OrdersRow(props: { orderData: PageOrderData; percentage: number }) { - const e = props?.orderData || {}; - const percentage = props?.percentage; - - 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); - - let sideText: string; - - if (!secondAssetUsdPrice) { - sideText = 'undefined'; - } else { - const value = secondAssetUsdPrice * e.price; - sideText = e.price < 0.9 ? `$${value.toFixed(5)}` : `$${value.toFixed(2)}`; - } - - return ( - - -

setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - @{cutAddress(e.user?.alias || 'no alias', 12)} - -

- {e.isInstant && } - {/* High volume */} - {/* */} - - {e.user?.alias.length > 12 && ( - - {e.user?.alias} - - )} - - - {notationToString(e.price)} - - {notationToString(e.amount)} - {notationToString(e.left)} - - {notationToString(totalDecimal.toString())} - - {/*

{localeTimeLeft(now, parseInt(e.expiration_timestamp, 10))}

*/} - - takeOrderClick(event, e)} - style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - width: '18px', - height: '18px', - }} - > - - - -
- - - ); - } - - function MyOrdersRow(props: { orderData: OrderRow }) { - const e = props?.orderData || {}; - 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) { - event.preventDefault(); - - setCancellingState(true); - const result = await cancelOrder(e.id); - - setCancellingState(false); - - if (!result.success) { - setAlertState('error'); - setAlertSubtitle('Error while cancelling order'); - setTimeout(() => { - setAlertState(null); - setAlertSubtitle(''); - }, 3000); - return; - } - - await updateOrders(); - await updateUserOrders(); - await fetchUser(); - } - - return ( - - -

setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - @ - {cutAddress( - state.wallet?.connected && state.wallet?.alias - ? state.wallet.alias - : 'no alias', - 12, - )} - -

- {e.isInstant && } - {(state.wallet?.connected && state.wallet?.alias ? state.wallet?.alias : '') - ?.length > 12 && ( - - {state.wallet?.connected && state.wallet?.alias} - - )} - {/* High volume */} - {/* */} - - - {notationToString(e.price)} - - {notationToString(e.amount)} - {notationToString(e.left)} - - {notationToString(totalDecimal.toString())} - - {/* {localeTimeLeft(now, parseInt(e.expiration_timestamp, 10))} */} - -

- {applyTips?.filter((tip) => tip.connected_order_id === e.id)?.length || 0} -

- - - - {cancellingState ? 'Process' : 'Cancel'} - - - - ); - } - - function MyOrdersApplyRow(props: { orderData: ApplyTip }) { - const e = props?.orderData || {}; - - 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) { - 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(); - } - - return ( - - -

setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > - @{cutAddress(e.user.alias, 12) || 'no alias'} - -

- {(e.isInstant || e.transaction) && } - - {e.user?.alias.length > 12 && ( - - {e.user?.alias} - - )} - {/* High volume */} - {/* */} - - - - {notationToString(e.price)} - - {notationToString(e.left)} - - - {notationToString(totalDecimal.toString())} - - {/* {localeTimeLeft(now, parseInt(e.expiration_timestamp, 10))} */} - - - - {applyingState ? 'Process' : 'Apply'} - - - - ); - } - - const imgCode = - pairData && tradingKnownCurrencies.includes(pairData.first_currency?.code) - ? pairData.first_currency?.code - : 'tsds'; - - const coefficient = pairStats?.coefficient || 0; - const coefficientOutput = - parseFloat(coefficient?.toFixed(2) || '0') === -100 - ? -99.99 - : parseFloat(coefficient?.toFixed(2) || '0'); - - const ordersIsBuy = ordersBuySell.code === 'buy'; - const shownOrdersAmount = filteredOrdersHistory.filter( - (e) => (e.type === 'buy') === ordersIsBuy, - ).length; - - const ordersSummaryFunds = filteredOrdersHistory - .reduce( - (acc, e) => acc.add(new Decimal(e.left).mul(new Decimal(e.price)).toNumber()), - new Decimal(0), - ) - .toDP(5) - .toFixed(); - - const pairRateUsd = - pairStats?.rate !== undefined && secondAssetUsdPrice !== undefined - ? new Decimal(pairStats.rate) - .mul(secondAssetUsdPrice) - .toFixed(pairStats.rate < 0.1 ? 6 : 2) - : undefined; - - const scrollToOrderList = () => { - if (!orderListRef.current) return; - - orderListRef.current.scrollIntoView({ behavior: 'smooth' }); + }, [userOrders, updateUserOrders]); + + const { filteredOrdersHistory } = useFilteredData({ + ordersBuySell, + ordersHistory, + }); + + const onAfter = async () => { + await updateOrders(); + await updateUserOrders(); + await fetchUser(); + await fetchTrades(); }; return ( <> -
-
-
- -
-
-
- currency -
-
-

- {!( - pairData && - pairData.first_currency?.name && - pairData.second_currency?.name - ) ? ( - '...' - ) : ( - <> - {firstCurrencyName} - /{secondCurrencyName} - - )} -

-
-

- {notationToString(pairStats?.rate || 0)}{' '} - {secondCurrencyName} -

- {pairRateUsd && ( - <> -
-

- ${pairRateUsd} -

- - )} -
-
-
- {pairData && firstAssetLink && secondAssetLink && ( -
-

- {firstCurrencyName}:{' '} - - {shortenAddress(firstAssetId || '')} - -

-

- {secondCurrencyName}:{' '} - - {shortenAddress(secondAssetId || '')} - -

-
- )} -
- -
- - - - -
-
+
-
-
- {InputPanelItem({ - priceState, - amountState, - totalState, - buySellValues, - buySellState, - setBuySellState, - setPriceFunction, - setAmountFunction, - setAlertState, - setAlertSubtitle, - setRangeInputValue, - rangeInputValue, - firstCurrencyName, - secondCurrencyName, - balance: Number(balance), - priceValid, - amountValid, - totalValid, - totalUsd, - scrollToOrderList, - updateUserOrders, - })} +
+ + +
+
+
-
-
+ +
+
- undefined} + isTab + isSm />
{candlesLoaded ? ( - + ) : ( - + )}
-
-
-
-
-
-
-
- {ordersBuySell.code === 'buy' ? 'Buy' : 'Sell'} Orders - {/* {firstCurrencyName && secondCurrencyName ? " - " + firstCurrencyName + "/" + secondCurrencyName : ""} */} -
-
-

- {firstCurrencyName && secondCurrencyName - ? `${firstCurrencyName}/${secondCurrencyName}` - : ''} -

-
- -
-
-

- {shownOrdersAmount}{' '} - {shownOrdersAmount === 1 ? 'order' : 'orders'} -

-
-
-

- {ordersSummaryFunds} {secondCurrencyName} -

-
-
-
- {/* */} - -
- -
- - - - - - - - - - - - {!ordersLoading && !!filteredOrdersHistory.length && ( - - {filteredOrdersHistory?.map((e) => { - const maxValue = Math.max( - ...filteredOrdersHistory.map((order) => - parseFloat(String(order.left)), - ), - ); - const percentage = ( - (parseFloat(String(e.left)) / maxValue) * - 100 - ).toFixed(2); - - return ( - - ); - })} - - )} -
Alias - Price
({secondCurrencyName}) -
- Amount
({firstCurrencyName}) -
- Remaining
({firstCurrencyName}) -
- {' '} - Total
({secondCurrencyName}) -
- - {!filteredOrdersHistory.length && !ordersLoading && ( -
- -
No orders
-
- )} - {ordersLoading && ( - - )} -
-
-
-
-
My Orders
- -
-

- {applyTips?.length || 0} Offer - {(applyTips?.length || 0) === 1 ? '' : 's'} -

-
-
- -
- - - - - - - - - - - - -
Alias - Price
({secondCurrencyName}) -
- Amount
({firstCurrencyName}) -
- Remaining
({firstCurrencyName}) -
- Total
({secondCurrencyName}) -
Offers
- {!myOrdersLoading && loggedIn && !!userOrders.length && ( -
- - - {userOrders.map((e) => ( - - ))} - -
- {!!applyTips.length && ( - - - {applyTips.map((e) => ( - - ))} - -
- )} -
- )} - - {myOrdersLoading && loggedIn && ( - - )} - - {!loggedIn && ( -
- -
Connect wallet to see your orders
-
- )} - - {loggedIn && !userOrders.length && !myOrdersLoading && ( -
- -
No orders
-
- )} -
+
+
+ + {alertState && ( { - const { first, second } = context.query; - - if (!first || !second) { - return { - notFound: true, // Show a 404 page if parameters are missing - }; - } - - try { - const idFound = await findPairID( - first as string, - second as string, - context.req.headers.host as string, - ); - - console.log('ID found:', idFound); - - if (typeof idFound === 'number') { - return { - redirect: { - destination: `/dex/trading/${idFound}`, - permanent: false, - }, - }; - } - - return { - notFound: true, - }; - } catch (error) { - console.error('Error fetching pair ID:', error); - return { - props: { - error: 'Failed to resolve the pair.', - }, - }; - } -}; - -export default getServerSideProps; diff --git a/src/store/store-reducer.tsx b/src/store/store-reducer.tsx index ee4edff..848ff98 100644 --- a/src/store/store-reducer.tsx +++ b/src/store/store-reducer.tsx @@ -10,6 +10,8 @@ const initialState: ContextState = { offers: 0, }, closed_notifications: [], + alertState: null, + alertSubtitle: '', }; const reducer = (state: ContextState, action: ContextAction): ContextState => { @@ -31,6 +33,12 @@ const reducer = (state: ContextState, action: ContextAction): ContextState => { case 'CLOSED_NOTIFICATIONS_UPDATED': { return { ...state, closed_notifications: action.payload }; } + case 'ALERT_STATE_UPDATED': { + return { ...state, alertState: action.payload }; + } + case 'ALERT_SUBTITLE_UPDATED': { + return { ...state, alertSubtitle: action.payload }; + } default: return { ...state }; } @@ -40,6 +48,7 @@ export const Store = createContext({ state: initialState, dispatch: () => undefined, }); + export const StoreProvider = (props: { children: ReactNode }) => { const [state, dispatch] = useReducer(reducer, initialState); return {props.children}; diff --git a/src/styles/Dex.module.scss b/src/styles/Dex.module.scss index bbadf08..6478dd5 100644 --- a/src/styles/Dex.module.scss +++ b/src/styles/Dex.module.scss @@ -8,7 +8,7 @@ height: 54px; padding: 0 12px; - > div { + >div { display: flex; align-items: center; gap: 10px; @@ -136,12 +136,12 @@ width: 240px; height: 42px; - > div:nth-child(1) { + >div:nth-child(1) { border-radius: 10px; padding: 11px 20px; - > div { - > div p { + >div { + >div p { font-size: 14px; font-weight: 500; } @@ -160,12 +160,12 @@ } } - > div:nth-child(2) { - > div { - > div { + >div:nth-child(2) { + >div { + >div { padding: 11px 20px; - > div { + >div { p { font-size: 14px; font-weight: 500; @@ -233,7 +233,7 @@ gap: 20px; .input_wrapper { - padding: 13px; + padding-inline: 13px; max-width: 240px; max-height: 42px; display: flex; @@ -244,7 +244,9 @@ border-radius: 10px; .input { - padding: 0; + border-radius: 0; + padding-inline: 0; + padding-block: 13px; max-width: 185px; background-color: transparent; font-size: 14px; @@ -303,7 +305,7 @@ left: 50%; transform: translateX(-50%); - > p { + >p { width: 100%; font-size: 16px; text-wrap: wrap; @@ -321,11 +323,11 @@ } } - > p { + >p { margin-right: 6px; } - > input { + >input { margin-left: 33px; height: 18px; width: 18px; @@ -387,7 +389,7 @@ left: 50%; transform: translateX(-50%); - > p { + >p { width: 100%; font-size: 16px; text-wrap: wrap; @@ -399,14 +401,14 @@ display: block; } - > svg > * { + >svg>* { opacity: 1; } } } } - > .curve__chart { + >.curve__chart { display: block; width: 100%; height: 50px; @@ -417,7 +419,7 @@ align-items: center; gap: 15px; - > div:first-child { + >div:first-child { width: 48px; height: 48px; padding: 10px; @@ -427,13 +429,13 @@ background: var(--icon-bg-color); border-radius: 50%; - > img { + >img { width: auto; height: 100%; } } - > div:last-child { + >div:last-child { height: 48px; display: flex; flex-direction: column; @@ -502,4 +504,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/styles/Trading.module.scss b/src/styles/Trading.module.scss index 270f4eb..4ba0c53 100644 --- a/src/styles/Trading.module.scss +++ b/src/styles/Trading.module.scss @@ -1,1107 +1,92 @@ -.main { +.trading { + padding-inline: 60px; + padding-top: 16px; display: flex; flex-direction: column; + gap: 8px; - @media screen and (max-width: 600px) { - margin-top: -50px; + @media screen and (max-width: 1600px) { + padding-inline: 40px; } - > :first-child { - padding-bottom: 40px; - border-bottom: 1px solid var(--delimiter-color); + @media screen and (max-width: 1200px) { + padding-inline: 20px; } - table { - .alias { - display: flex; - align-items: center; - gap: 4px; - - p { - font-size: 14px; - } - - path { - fill: none; - } - - &__tooltip { - position: absolute; - top: 30px; - left: 5%; - background-color: var(--trade-table-tooltip); - font-size: 12px; - - &_arrow { - border-radius: 2px; - left: 50%; - background-color: var(--trade-table-tooltip); - } - } - } - } - - .trading__title__wrapper { + &__top { + margin-top: 16px; display: flex; - flex-direction: column; - gap: 25px; - position: relative; - border-bottom: 1px solid var(--delimiter-color); + gap: 10px; + height: 550px; - .currency__stats__wrapper { - max-width: 900px; - display: flex; - flex-wrap: wrap; - gap: 20px; - - > :nth-child(2), - :nth-child(4) { - padding-left: 40px; - border-left: 1px solid var(--delimiter-color); - } - - .trading__stat__item { - padding-top: 10px; - padding-bottom: 10px; - display: flex; - flex-direction: column; - gap: 16px; - width: 47%; - - > div { - display: flex; - align-items: center; - - p { - white-space: nowrap; - } - - &:first-child { - p { - color: var(--font-dimmed-color); - } - - gap: 6px; - } - - &:last-child { - gap: 10px; - - p:first-child { - font-weight: 600; - } - - .coefficient__green { - color: #16d1d6; - } - - .coefficient__red { - color: #ff6767; - } - } - } - } - - @media screen and (min-width: 1700px) { - flex-wrap: nowrap; - gap: 40px; - position: absolute; - transform: translateX(-50%); - left: 54%; - - > div { - width: auto !important; - } - - > :nth-child(3) { - padding-left: 40px; - border-left: 1px solid var(--delimiter-color); - } - } - - @media screen and (max-width: 600px) { - gap: 0; - flex-wrap: nowrap; - flex-direction: column; - - > div { - width: 100% !important; - padding-left: 0 !important; - border-left: none !important; - - padding: 20px 0 !important; - border-bottom: 1px solid var(--delimiter-color); - - &:last-child { - border-bottom: none; - padding-bottom: 0 !important; - margin-bottom: -20px; - } - } - } - } - - .trading__currency__wrapper { - display: flex; - flex-direction: column; - gap: 15px; - - .trading__currency__wrapper_top { - display: flex; - align-items: center; - gap: 16px; - - > div:first-child { - width: 68px; - height: 68px; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--icon-bg-color); - border-radius: 50%; - - img { - width: 40px; - height: auto; - } - } - - > div:last-child { - display: flex; - flex-direction: column; - justify-content: space-between; - - > p:first-child { - font-size: 40px; - font-weight: 500; - - span { - font-size: 40px; - color: var(--font-dimmed-color); - } - } - - .trading__currency__rate { - display: flex; - align-items: center; - gap: 10px; - - > div { - background-color: var(--font-dimmed-color); - width: 1px; - height: 16px; - } - - > p { - font-weight: 600; - } - - .trading__currency__rate_usd { - color: var(--font-dimmed-color); - } - } - } - - @media screen and (max-width: 400px) { - > div:first-child { - width: 48px; - height: 48px; - - > img { - scale: 0.7; - } - } - - > div:last-child { - > p:first-child { - font-size: 24px; - - > span { - font-size: 24px; - } - } - } - } - } - - .trading__currency__wrapper_bottom { - display: flex; - flex-direction: column; - gap: 5px; - } - } - } - - .trading__top__wrapper { - display: flex; - gap: 20px; - padding-bottom: 40px; - - > div:first-child { - padding: 30px; - width: 50%; - margin-top: 25px; - border-radius: 10px; - border: 1px solid var(--delimiter-color); - background: var(--window-bg-color); - } - - @media screen and (max-width: 1000px) { - flex-direction: column; - - > * { - width: 100% !important; - } - } - } - - .trading__chart__wrapper { - width: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - - &.mobile { - display: none; - } - - .trading__chart__preloader { + &_trades { + max-width: 415px; + width: 100%; height: 100%; - min-height: 545px; } - .trading__chart__settings { + &_chart { + width: 100%; + overflow: hidden; display: flex; - align-items: center; - justify-content: space-between; - padding: 25px 0; - border-bottom: 1px solid var(--delimiter-color); + flex-direction: column; - .trading__chart__dropdown { - width: 254px; - } - } - - @media screen and (max-width: 1000px) { - &.mobile { - display: flex; - } - - &.desktop { - display: none; - } - } - } - - .trading__info { - display: flex; - gap: 40px; - - > * { - width: 100%; - } - - > div { - padding: 30px; - - background: var(--window-bg-color); - border: 1px solid var(--delimiter-color); - border-radius: 10px; - - > :first-child { - margin-right: 30px; - padding-bottom: 20px; - border-bottom: 1px solid var(--delimiter-color); - } - - > :last-child { - padding-top: 30px; - } - } - - .orders__preloader { - height: 465px; - padding-right: 30px; - } - - .trading__orders_panel { - width: 100%; - - padding-right: 0; - - .orders-panel__header { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - column-gap: 15px; - row-gap: 10px; - - .orders-panel__header__select { - overflow: initial; - } - - .orders-panel__header_left { - display: flex; - align-items: center; - flex-wrap: wrap; - column-gap: 10px; - row-gap: 5px; - - .header__delimiter { - width: 2px; - height: 18px; - margin-left: 17px; - margin-right: 14px; - background-color: #525e85; - } - - .header__orders_buy, - .header__orders_sell { - padding: 6px 12px; - border-radius: 8px; - - > p { - font-size: 14px; - font-weight: 400; - } - } - - .header__orders_buy { - border: 1px solid #16d1d6; - - > p { - color: #16d1d6; - } - } - - .header__orders_sell { - border: 1px solid #ff6767; - - > p { - color: #ff6767; - } - } - - > div:first-child { - display: flex; - align-items: center; - - > p { - font-size: 21px; - font-weight: 500; - color: var(--font-dimmed-color); - } - } - - .header__stats { - display: flex; - align-items: center; - gap: 5px; - - .header__summary-funds { - padding: 6px 12px; - border: 1px solid var(--font-dimmed-color); - border-radius: 8px; - - > p, - > p > span { - font-size: 14px; - color: var(--font-dimmed-color); - } - - > p { - display: flex; - gap: 5px; - } - - > p > span { - display: inline-block; - max-width: 70px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - } - - @media screen and (max-width: 640px) { - .orders-panel__header_left { - > div:first-child { - > p { - font-size: 14px; - } - } - - .header__delimiter { - margin-left: 12px; - margin-right: 12px; - } - } - } - } - - > div:last-child { - display: flex; - flex-direction: column; - } - - table { - width: 100%; - padding-right: 12px; - - thead { - display: flex; - width: 100%; - padding-right: 18px; - margin-bottom: 8px; - - tr { - width: 100%; - display: flex; - - th { - width: 100%; - font-size: 12px; - font-weight: 700; - text-align: start; - color: var(--table-th-color); - - br { - display: none; - } - - &:first-child { - max-width: 180px; - } - - &:last-child { - max-width: 10px; - } - } - } - } - - tbody { - height: 465px; - display: flex; - flex-direction: column; - overflow: auto; - scrollbar-gutter: stable; - padding-right: 12px; - padding-bottom: 50px; - - tr { - position: relative; - width: 100%; - display: flex; - padding: 10px 0; - align-items: center; - - &::after { - content: ''; - pointer-events: none; - position: absolute; - z-index: 1; - right: 0; - top: 0; - width: var(--line-width, 0%); - height: 100%; - background: #16d1d61a; - } - - &.sell_section { - &::after { - background: #ff67671a; - } - } - - &:not(:last-child) { - border-bottom: 1px solid var(--delimiter-color); - } - - td { - flex: 1; - min-width: 0; - position: relative; - - > p { - overflow: hidden; - text-overflow: ellipsis; - line-height: 1; - font-size: 14px; - - > span { - margin: 5px; - line-height: 1; - vertical-align: middle; - color: var(--font-dimmed-color); - font-size: 12px; - } - } - - &:first-child { - max-width: 180px; - display: flex; - flex-direction: column; - gap: 3px; - } - - &:last-child { - text-align: end; - max-width: 10px; - margin-right: 10px; - } - - svg:not(.stroked) * { - fill: transparent; - } - - svg { - transform: scale(1.2); - } - - .orders_table__buy { - &:hover { - svg path { - stroke: #56c2c6a8; - } - } - } - - .orders_table__sell { - svg { - path { - stroke: #ff6767; - } - } - - &:hover { - svg path { - stroke: #ff8585; - } - } - } - } - } - } - } - } - - .trading__user__orders { - padding-right: 0; - - th { - br { - display: none; - } - } - - > div:first-child { - margin-right: 30px; + .settings { display: flex; align-items: center; - gap: 10px; + justify-content: space-between; - > div { - padding: 5px 10px; - background-color: #ff6767; - border-radius: 100px; + &__dropdown { + width: 250px; + height: 48px; - > p { - font-size: 12px; - font-weight: 700; - color: #fff; - } - } - } - - > div:last-child { - padding-right: 12px; - } - - table { - width: 100%; - padding-right: 33px; - - thead { - display: flex; - width: 100%; - padding-left: 15px; - margin-bottom: 8px; - - tr { - width: 100%; - display: flex; - - th { - width: 100%; - font-size: 12px; - font-weight: 700; - text-align: start; - color: var(--table-th-color); - - &:first-child { - max-width: 112px; - display: flex; - flex-direction: column; - gap: 3px; - } - - &:nth-child(3) { - width: 120%; - } - - &:nth-last-child(2) { - max-width: 50px; - } - - &:last-child { - max-width: 80px; - } - } - } - } - - tbody { - p, - a { - font-size: 14px; - } - } - } - - .trading__right__tables { - padding-bottom: 50px; - scrollbar-gutter: stable; - padding-right: 12px; - height: 465px; - overflow: auto; - - table { - width: 100%; - padding: 0 15px; - - &.trading__apply__table { - border: 1px solid rgba(31, 143, 235, 1); - border-radius: 10px; - background: var(--blur-color); - } - - tbody { - display: flex; - flex-direction: column; - - > div:first-child { - padding-left: 15px; - padding-right: 10px; - } - - .stats__table__incoming { - padding-left: 15px; - padding-right: 10px; - border: 1px solid rgba(31, 143, 235, 1); - border-radius: 10px; - background: rgba(255, 255, 255, 0.05); - } - - tr { - width: 100%; - display: flex; - align-items: center; - padding: 10px 0; - - &:not(:last-child) { - border-bottom: 1px solid var(--delimiter-color); - } - - td { - width: 100%; - min-width: 0; - position: relative; - - > p { - overflow: hidden; - text-overflow: ellipsis; - line-height: 1; - - > span { - margin: 5px; - vertical-align: middle; - line-height: 1; - color: var(--font-dimmed-color); - font-size: 12px; - margin: 5px; - } - } - - &:first-child { - max-width: 112px; - display: flex; - flex-direction: column; - gap: 3px; - } - - &:nth-child(3) { - width: 120%; - } - - &:nth-last-child(2) { - max-width: 50px; - } - - &:last-child { - text-align: end; - max-width: 80px; - } - } - } + @media screen and (max-width: 1360px) { + width: 200px; } } } } - .orders__message { + &_form { width: 100%; - height: 465px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; - - &.all__orders__msg { - padding-right: 30px; - } - - &.user__orders__msg { - padding-right: 18px; - } - - @media screen and (max-width: 550px) { - &.all__orders__msg { - padding-right: 20px; - } - - &.user__orders__msg { - padding-right: 13px; - } - } - - > h6 { - color: var(--font-dimmed-color); - } - } - } - - .table__tooltip { - position: absolute; - top: 30px; - left: 20%; - transform: translateX(-50%); - background-color: var(--trade-table-tooltip); - - > .table__tooltip_arrow { - background-color: var(--trade-table-tooltip); - } - } - - .table__tooltip_right { - position: absolute; - top: 30px; - left: 2%; - background-color: var(--trade-table-tooltip); - - > .table__tooltip_arrow { - left: 40px; - background-color: var(--trade-table-tooltip); - } - } - - .table__tooltip_end { - position: absolute; - top: 30px; - left: -50%; - background-color: var(--trade-table-tooltip); - - > .table__tooltip_arrow { - border-radius: 2px; - left: 10%; - background-color: var(--trade-table-tooltip); - } - } - - .badge { - width: fit-content; - padding: 1px 4px; - padding-right: 8px; - display: flex; - align-items: center; - gap: 2px; - border-radius: 100px; - background: radial-gradient(100% 246.57% at 0% 0%, #a366ff 0%, #601fff 100%); - - > img { - height: 13px; - width: auto; - } - - > span { - font-size: 10px; - font-weight: 600; - } - - &.high { - padding: 2px 4px; - background: radial-gradient(100% 188.88% at 0% 0%, #16d1d6 0%, #274cff 100%); - } - } - - @media screen and (max-width: 1750px) { - .trading__info { - flex-direction: column; - - > div:last-child { - width: 100%; - min-width: 100%; - } - } - } - - @media screen and (max-width: 900px) { - .trading__orders_panel { - table { - th, - td { - &:nth-child(1) { - padding-right: 20px; - } - } - } - } - - .trading__user__orders { - > div:last-child { - table { - td, - th { - &:first-child { - display: none; - } - } - } - } - } - } - - @media screen and (max-width: 700px) { - .trading__user__orders { - table { - td > a, - td > p { - font-size: 14px; - - > span { - display: none; - } - } - - td, - th { - &:nth-last-child(4) { - display: none; - } - } - - tbody { - tr { - padding: 12px 0 !important; - } - } - } - } - - .trading__orders_panel { - table { - td > a, - td > p { - font-size: 14px; - - > span { - display: none; - } - } - - th, - td { - &:nth-last-child(3) { - display: none; - } - } - - tbody { - tr { - padding: 12px 0 !important; - } - } - } - } - } - - @media screen and (max-width: 600px) { - .trading__user__orders th br { - display: block !important; - } - } - - @media screen and (max-width: 550px) { - .trading__top__wrapper { - > div:first-child { - padding: 20px !important; - } - } - - .trading__info { - > div { - padding: 20px 0; - } - - .trading__user__orders { - width: 100%; - min-width: 100%; - padding: 20px; - padding-right: 0; - - > div:first-child { - margin-right: 20px; - } - - > div:last-child { - padding-right: 7px !important; - - > table:first-child { - padding-right: 26px !important; - } - } - - table td { - font-size: 14px; - } - - thead { - padding-left: 10px !important; - - th { - font-size: 11px; - } - } - - .trading__right__tables { - padding-right: 7px !important; - - table { - padding: 0 10px !important; - } - } - } - - .trading__orders_panel { - padding-left: 20px !important; - - thead { - padding-right: 13px !important; - } - - > div:first-child { - margin-right: 20px; - } - - > :last-child { - flex-direction: column; - } - - > input[type='number'], - > button { - padding-top: 13px !important; - padding-bottom: 13px !important; - } - - > div:first-child { - h5 { - span { - display: none; - } - } - } - - table { - padding-right: 7px !important; - - thead { - tr th br { - display: block !important; - } - } - - tbody { - padding-right: 7px !important; - } - } - } - } - - .trading__chart__wrapper { - .trading__chart__settings { - padding-top: 40px; - flex-direction: column-reverse; - align-items: flex-start !important; - gap: 40px; - - .trading__chart__dropdown { - width: 100%; - - > div:first-child { - height: 48px; - } - } - } - } - } - - @media screen and (max-width: 450px) { - .trading__info { - .trading__orders_panel { - .orders__preloader { - padding-right: 20px !important; - } - - table { - td > a, - td > p, - th { - font-size: 11 px; - } - - thead tr th { - font-size: 9px; - } - } - } - - .trading__user__orders { - table { - thead tr th { - font-size: 9px; - } - - tbody { - td > p, - td > a { - font-size: 11px; - } - } - } - } + height: 100%; + max-width: 415px; } } } + +@media screen and (max-width: 1280px) { + .trading__top { + gap: 8px; + + &_trades, + &_form { + max-width: 310px; + } + } +} + +@media screen and (max-width: 1024px) { + .trading__top { + &_trades, + &_form { + max-width: 100%; + } + + &_chart { + display: none; + } + } +} + +@media screen and (max-width: 640px) { + .trading__top { + height: 380px; + } +} + +@media screen and (max-width: 580px) { + .trading__top { + height: 370px; + } +} diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 601f3f7..97e3749 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -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 { diff --git a/src/styles/themes/dark.scss b/src/styles/themes/dark.scss new file mode 100644 index 0000000..de9a109 --- /dev/null +++ b/src/styles/themes/dark.scss @@ -0,0 +1,59 @@ +[data-theme='dark'] { + --main-bg-color: #0c0c3a; + --table-header-font-color: rgba(213, 213, 226, 1); + --table-header-bg: rgba(36, 36, 78, 1); + --table-button-bg-hover: rgba(40, 40, 83, 1); + --font-main-color: rgba(255, 255, 255, 1); + --font-dimmed-color: rgba(141, 149, 174, 1); + --delimiter-color: rgba(255, 255, 255, 0.1); + --window-bg-color: rgba(15, 32, 85, 1); + --dropdown-bg-color: rgba(17, 49, 107, 1); + --dropdown-bg-hover: rgba(29, 59, 114, 1); + --profile-widget-avatar: rgba(17, 49, 107, 1); + --window-border-color: rgba(255, 255, 255, 0.3); + --bordered-input-bg: rgba(255, 255, 255, 0.1); + --row-header-bg: #154d91; + --button-bordered-hover: rgba(255, 255, 255, 0.1); + --alert-bg: rgba(35, 52, 103, 1); + --switch-bg-color: rgba(15, 32, 85, 1); + --switch-bg-hover: rgba(39, 54, 102, 1); + --switch-disabled-bg-color: rgba(255, 255, 255, 0.1); + --dimmed-btn-bg: rgba(255, 255, 255, 0.1); + --dimmed-btn-hover: rgba(255, 255, 255, 0.3); + --font-faded-color: rgba(255, 255, 255, 0.5); + --slider-bg-color: #1d3b72; + --blur-color: rgba(255, 255, 255, 0.05); + --icon-bg-color: #0f1f54; + --swap-btn-bg: rgba(31, 143, 235, 0.1); + --table-bg-color: rgba(16, 16, 64, 1); + --advices-bg-color: rgba(255, 255, 255, 0.1); + --messenger-top-bg: #0f2055; + --messenger-bottom-bg: #273666; + --messenger-bg: #0f2055; + --messenger-border: rgba(255, 255, 255, 0.1); + --message-bg: rgba(255, 255, 255, 0.1); + --custom-message-bg: transparent; + --trade-table-tooltip: #1d3b72; + --dex-offer-notification: #0f2055; + --dex-panel-bg: #0f2055; + --dex-panel-tooltip: #273666; + --dex-buy-sell-border: #ffffff1a; + --dex-input-currency: #39497c; + --footer-selected-link: #ffffffcc; + --table-th-color: #ffffffb2; + --table-thead-bg: #24244e; + --table-tbody-bg: #101040; + --admin-table-border-color: #596f98; + --alert-btn-bg: rgba(31, 143, 235, 0.2); + --alert-btn-hover: rgba(31, 143, 235, 0.3); + --table-even-bg: #0c1d4f; + --table-tr-hover-color: #172a66; + --dex-tooltip-bg: #11316b; + --dex-tooltip-border-color: #1f8feb26; + --dex-sell-percentage: #272757; + --dex-buy-percentage: #103262; + --action-btn-bg: #273666; + --table-group-header-bg: #0c1940; + --tab-bg-color: #1f8feb1a; + --selector-bg-color: #0c1940; +} diff --git a/src/styles/themes/light.scss b/src/styles/themes/light.scss index b8eee08..8a639d6 100644 --- a/src/styles/themes/light.scss +++ b/src/styles/themes/light.scss @@ -46,54 +46,14 @@ --admin-table-border-color: rgba(31, 143, 235, 0.2); --alert-btn-bg: rgba(31, 143, 235, 0.2); --alert-btn-hover: rgba(31, 143, 235, 0.3); -} - -[data-theme='dark'] { - --main-bg-color: #0c0c3a; - --table-header-font-color: rgba(213, 213, 226, 1); - --table-header-bg: rgba(36, 36, 78, 1); - --table-button-bg-hover: rgba(40, 40, 83, 1); - --font-main-color: rgba(255, 255, 255, 1); - --font-dimmed-color: rgba(141, 149, 174, 1); - --delimiter-color: rgba(255, 255, 255, 0.1); - --window-bg-color: rgba(15, 32, 85, 1); - --dropdown-bg-color: rgba(17, 49, 107, 1); - --dropdown-bg-hover: rgba(29, 59, 114, 1); - --profile-widget-avatar: rgba(17, 49, 107, 1); - --window-border-color: rgba(255, 255, 255, 0.3); - --bordered-input-bg: rgba(255, 255, 255, 0.1); - --row-header-bg: #154d91; - --button-bordered-hover: rgba(255, 255, 255, 0.1); - --alert-bg: rgba(35, 52, 103, 1); - --switch-bg-color: rgba(15, 32, 85, 1); - --switch-bg-hover: rgba(39, 54, 102, 1); - --switch-disabled-bg-color: rgba(255, 255, 255, 0.1); - --dimmed-btn-bg: rgba(255, 255, 255, 0.1); - --dimmed-btn-hover: rgba(255, 255, 255, 0.3); - --font-faded-color: rgba(255, 255, 255, 0.5); - --slider-bg-color: #1d3b72; - --blur-color: rgba(255, 255, 255, 0.05); - --icon-bg-color: #0f1f54; - --swap-btn-bg: rgba(31, 143, 235, 0.1); - --table-bg-color: rgba(16, 16, 64, 1); - --advices-bg-color: rgba(255, 255, 255, 0.1); - --messenger-top-bg: #0f2055; - --messenger-bottom-bg: #273666; - --messenger-bg: #0f2055; - --messenger-border: rgba(255, 255, 255, 0.1); - --message-bg: rgba(255, 255, 255, 0.1); - --custom-message-bg: transparent; - --trade-table-tooltip: #1d3b72; - --dex-offer-notification: #0f2055; - --dex-panel-bg: #0f2055; - --dex-panel-tooltip: #273666; - --dex-buy-sell-border: #ffffff1a; - --dex-input-currency: #39497c; - --footer-selected-link: #ffffffcc; - --table-th-color: #ffffffb2; - --table-thead-bg: #24244e; - --table-tbody-bg: #101040; - --admin-table-border-color: #596f98; - --alert-btn-bg: rgba(31, 143, 235, 0.2); - --alert-btn-hover: rgba(31, 143, 235, 0.3); + --table-even-bg: #f2f5f9; + --table-tr-hover-color: #dcf0ff; + --dex-tooltip-bg: #eff8ff; + --dex-tooltip-border-color: #1f8feb33; + --dex-sell-percentage: #fceded; + --dex-buy-percentage: #e5f8f8; + --action-btn-bg: #d0e6f9; + --table-group-header-bg: #f2f5f9; + --tab-bg-color: #1f8feb; + --selector-bg-color: #e7eff8; } diff --git a/src/utils/handleInputChange.ts b/src/utils/handleInputChange.ts new file mode 100644 index 0000000..7a7f35d --- /dev/null +++ b/src/utils/handleInputChange.ts @@ -0,0 +1,94 @@ +import { Dispatch, SetStateAction } from 'react'; +import Decimal from 'decimal.js'; +import { isPositiveFloatStr } from '@/utils/utils'; +import { validateTokensInput } from 'shared/utils'; + +type SetStr = (_v: string) => void; +interface HandleInputChangeParams { + inputValue: string; + priceOrAmount: 'price' | 'amount'; + otherValue: string; + thisDP: number; + totalDP: number; + setThisState: SetStr; + setTotalState: SetStr; + setThisValid: Dispatch>; + setTotalValid: Dispatch>; + balance?: string | undefined; + setRangeInputValue?: Dispatch>; +} + +export function handleInputChange({ + inputValue, + priceOrAmount, + otherValue, + thisDP, + totalDP, + setThisState, + setTotalState, + setThisValid, + setTotalValid, + balance, + setRangeInputValue, +}: HandleInputChangeParams) { + if (inputValue !== '' && !isPositiveFloatStr(inputValue)) return; + + const digitsOnly = inputValue.replace('.', '').replace(/^0+/, ''); + if (digitsOnly.length > 18) return; + + let thisDecimal: Decimal; + let otherDecimal: Decimal; + + try { + thisDecimal = new Decimal(inputValue || '0'); + otherDecimal = new Decimal(otherValue || '0'); + } catch (err) { + console.log(err); + setThisValid(false); + setTotalValid(false); + return; + } + + setThisState(inputValue); + + if (!inputValue) { + setTotalState(''); + setTotalValid(false); + setThisValid(false); + return; + } + + const isValid = validateTokensInput(inputValue, thisDP); + if (!isValid.valid) { + setTotalState(''); + setTotalValid(false); + setThisValid(false); + return; + } + + setThisValid(true); + + if (!thisDecimal.isNaN() && !otherDecimal.isNaN() && otherValue !== '') { + const rawTotal = + priceOrAmount === 'price' + ? thisDecimal.mul(otherDecimal) + : otherDecimal.mul(thisDecimal); + + const totalClamped = rawTotal.toDecimalPlaces(totalDP, Decimal.ROUND_DOWN); + + setTotalState(totalClamped.toString()); + + const fmtOk = validateTokensInput(totalClamped.toFixed(totalDP), totalDP).valid; + const gtZero = totalClamped.gt(0); + setTotalValid(fmtOk && gtZero); + + if (priceOrAmount === 'amount' && balance && setRangeInputValue) { + const bal = new Decimal(balance || '0'); + const percent = bal.gt(0) ? thisDecimal.div(bal).mul(100) : new Decimal(0); + setRangeInputValue(percent.toFixed()); + } + } else { + setTotalState(''); + setTotalValid(false); + } +} diff --git a/src/utils/methods.ts b/src/utils/methods.ts index db63ea0..476bec0 100644 --- a/src/utils/methods.ts +++ b/src/utils/methods.ts @@ -320,6 +320,22 @@ export async function getChatChunk( .then((res) => res.data); } +export async function getTrades(pairId: string) { + return axios + .post(`/api/orders/get-trades`, { + pairId, + }) + .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( @@ -327,3 +343,15 @@ export async function getZanoPrice() { ) .then((res) => res.data); } + +export async function getMatrixAddresses(addresses: string[]) { + try { + const { data } = await axios.post('https://messenger.zano.org/api/get-addresses', { + addresses, + }); + + return data; + } catch (error) { + console.log(error); + } +} diff --git a/src/utils/takeOrderClick.ts b/src/utils/takeOrderClick.ts new file mode 100644 index 0000000..e2a081a --- /dev/null +++ b/src/utils/takeOrderClick.ts @@ -0,0 +1,66 @@ +import { PageOrderData } from '@/interfaces/responses/orders/GetOrdersPageRes'; +import { notationToString } from '@/utils/utils'; +import Decimal from 'decimal.js'; +import React from 'react'; +import PairDataType from '@/interfaces/common/PairData'; +import OrderFormOutput from '@/interfaces/common/orderFormOutput'; +import { handleInputChange } from './handleInputChange'; + +interface takeOrderClickParams { + event: + | React.MouseEvent + | React.MouseEvent; + PageOrderData: PageOrderData; + pairData: PairDataType | null; + orderForm: OrderFormOutput; + balance: string | undefined; + scrollToOrderForm: () => void; +} + +function takeOrderClick({ + event, + PageOrderData, + pairData, + orderForm, + balance, + scrollToOrderForm, +}: takeOrderClickParams) { + event.preventDefault(); + const e = PageOrderData; + + const priceStr = notationToString(new Decimal(e.price).toString()) || ''; + const amountStr = notationToString(new Decimal(e.left).toString()) || ''; + + const secondCurrencyDP = pairData?.second_currency?.asset_info?.decimal_point || 12; + const firstCurrencyDP = pairData?.first_currency?.asset_info?.decimal_point || 12; + + handleInputChange({ + inputValue: priceStr, + priceOrAmount: 'price', + otherValue: amountStr, + thisDP: secondCurrencyDP, + totalDP: secondCurrencyDP, + setThisState: orderForm.setPrice, + setTotalState: orderForm.setTotal, + setThisValid: orderForm.setPriceValid, + setTotalValid: orderForm.setTotalValid, + }); + + handleInputChange({ + inputValue: amountStr, + priceOrAmount: 'amount', + otherValue: priceStr, + thisDP: firstCurrencyDP, + totalDP: secondCurrencyDP, + setThisState: orderForm.setAmount, + setTotalState: orderForm.setTotal, + setThisValid: orderForm.setAmountValid, + setTotalValid: orderForm.setTotalValid, + balance, + setRangeInputValue: orderForm.setRangeInputValue, + }); + + scrollToOrderForm(); +} + +export default takeOrderClick; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index deb3643..a41d021 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -86,13 +86,14 @@ export const roundTo = (x: number | string, digits = 7) => { return fixedValue.replace(/(\.\d*?[1-9])0+$/g, '$1').replace(/\.0+$/, ''); }; -export const notationToString = (notation: number | string) => { +export const notationToString = (notation: number | string, fixed?: number) => { const decimalValue = new Decimal(notation || '0'); - const fixedValue = decimalValue.toFixed(); + if (fixed !== undefined) { + return decimalValue.toFixed(fixed).replace(/\.?0+$/, ''); + } - // Remove trailing zeros - return fixedValue; + return decimalValue.toFixed(); }; export const localeTimeLeft = (now: number | null, timestamp: number) => { @@ -133,9 +134,35 @@ export function isPositiveFloatStr(input: string) { return regExp.test(input); } -export function classes(...items: (string | boolean | undefined)[]): string { +export function formatTime(ts: string | number) { + let num = Number(ts); + + if (num < 1e12) num *= 1000; + const date = new Date(num); + + if (Number.isNaN(date.getTime())) return '-'; + + 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 `${hh}:${mm}:${ss} ${DD}-${MM}-${YYYY}`; +} + +export function classes(...classes: (string | boolean | undefined)[]): string { // boolean for constructions like [predicate] && [className] - return items.filter((className) => className).join(' '); + return classes.filter((className) => className).join(' '); } export const getAssetIcon = (assetId: string): string => { @@ -147,6 +174,38 @@ export const getAssetIcon = (assetId: string): string => { return '/tokens/token.png'; }; +type Getters = { + getSide: (_item: T) => 'buy' | 'sell'; + getPrice: (_item: T) => string | number | Decimal; +}; + +export function createOrderSorter({ getSide, getPrice }: Getters) { + return (a: T, b: T) => { + const aSide = getSide(a); + const bSide = getSide(b); + + if (aSide !== bSide) { + return aSide === 'sell' ? -1 : 1; + } + + const ap = new Decimal(getPrice(a)); + const bp = new Decimal(getPrice(b)); + + return bp.comparedTo(ap); + }; +} + +export function countByKeyRecord( + array: T[], + keySelector: (_item: T) => string | number, +): Record { + return array.reduce>((acc, item) => { + const key = String(keySelector(item)); + acc[key] = (acc[key] ?? 0) + 1; + return acc; + }, {}); +} + export const ZANO_ASSET_ID = 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a'; const knownCurrencies = ['zano', 'xmr', 'btc', 'firo', 'usd', 'eur', 'cad', 'jpy'];