- Coin: Zano → Lethean, ticker: ZAN/ZANO → LTHN - Ports: 11211 → 36941 (mainnet RPC), 46941 (testnet RPC) - Wallet: 11212 → 36944/46944 - Address prefix: iTHN - URLs: zano.org → lethean.io - Explorer links: explorer.lthn.io Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
import logoImg from '@/assets/images/UI/logo_block.svg?url';
|
|
import logoImgWhite from '@/assets/images/UI/logo_block_dark.svg?url';
|
|
import LogOutIcon from '@/assets/images/UI/logout.svg';
|
|
import BurgerIcon from '@/assets/images/UI/hamburger_icon.svg';
|
|
import BurgerCrossIcon from '@/assets/images/UI/burger_cross.svg';
|
|
import React, { useRef, useState, useEffect, useContext } from 'react';
|
|
import EyeIcon from '@/assets/images/UI/eye.svg';
|
|
import EyeCloseIcon from '@/assets/images/UI/eye_close.svg';
|
|
import letheanIcon from '@/assets/images/UI/lethean.svg?url';
|
|
import bitcoinWhiteIcon from '@/assets/images/UI/wbtc.svg?url';
|
|
import sunIcon from '@/assets/images/UI/sun_icon.svg?url';
|
|
import moonIcon from '@/assets/images/UI/moon_icon.svg?url';
|
|
import ethWhiteIcon from '@/assets/images/UI/weth.svg?url';
|
|
import customWhiteIcon from '@/assets/images/UI/tsds.svg?url';
|
|
import Link from 'next/link';
|
|
import Tooltip from '@/components/UI/Tooltip/Tooltip';
|
|
import Button from '@/components/UI/Button/Button';
|
|
|
|
import { useWindowWidth } from '@react-hook/window-size';
|
|
import ConnectButton from '@/components/UI/ConnectButton/ConnectButton';
|
|
import { classes, notationToString, shortenAddress } from '@/utils/utils';
|
|
import useAdvancedTheme from '@/hook/useTheme';
|
|
|
|
import { Store } from '@/store/store-reducer';
|
|
import { updateAutoClosedNotification, updateWalletState } from '@/store/actions';
|
|
import CurrencyCheckRowProps from '@/interfaces/props/components/default/Header/CurrencyCheckRowProps';
|
|
import Decimal from 'decimal.js';
|
|
import socket from '@/utils/socket';
|
|
import { OrderDataWithPair } from '@/interfaces/responses/orders/GetOrdersPageRes';
|
|
import { useRouter } from 'next/router';
|
|
import letheanImg from '@/assets/images/UI/lethean.svg?url';
|
|
import useUpdateUser from '@/hook/useUpdateUser';
|
|
import NavBar from './NavBar/NavBar';
|
|
import styles from './Header.module.scss';
|
|
|
|
function Header({ isLg }: { isLg?: boolean }) {
|
|
const { theme, setTheme } = useAdvancedTheme();
|
|
const router = useRouter();
|
|
|
|
const eyeRef = useRef<HTMLDivElement>(null);
|
|
|
|
const { dispatch, state } = useContext(Store);
|
|
const loggedIn = !!state.wallet?.connected;
|
|
|
|
const [menuOpened, setMenuState] = useState(false);
|
|
const [currencyCheckOpended, setCurrencyState] = useState(false);
|
|
const [balanceSeen, setBalanceState] = useState(true);
|
|
|
|
const width = useWindowWidth();
|
|
|
|
useEffect(() => {
|
|
setMenuState(false);
|
|
}, [width]);
|
|
|
|
function logout() {
|
|
sessionStorage.removeItem('token');
|
|
updateWalletState(dispatch, null);
|
|
}
|
|
|
|
const getIcon = (ticker: string) => {
|
|
switch (ticker) {
|
|
case 'WBTC':
|
|
return bitcoinWhiteIcon;
|
|
case 'WETH':
|
|
return ethWhiteIcon;
|
|
default:
|
|
return customWhiteIcon;
|
|
}
|
|
};
|
|
|
|
function BurgerButton(props: { className?: string }) {
|
|
return (
|
|
<Button
|
|
transparent
|
|
className={`${styles.header__account__btn} ${styles.burger__tablet__btn} ${props.className}`}
|
|
onClick={() => setMenuState(!menuOpened)}
|
|
>
|
|
{!menuOpened ? (
|
|
<BurgerIcon className="filled" />
|
|
) : (
|
|
<BurgerCrossIcon className="filled" />
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
function Menu(props: { isMobile?: boolean } = {}) {
|
|
function CurrencyCheck() {
|
|
function Row({ icon, title, amount, balanceSeen }: CurrencyCheckRowProps) {
|
|
const [textHovered, setTextHovered] = useState(false);
|
|
|
|
const displayedAmount = balanceSeen ? new Decimal(amount).toFixed() : '****';
|
|
|
|
const showTooltip = displayedAmount.length >= 7;
|
|
|
|
return (
|
|
<div
|
|
className={styles.currency__check__row}
|
|
onMouseEnter={() => setTextHovered(true)}
|
|
onMouseLeave={() => setTextHovered(false)}
|
|
>
|
|
<div className={styles.currency__check__icon}>
|
|
<img src={icon} alt={title}></img>
|
|
</div>
|
|
<p className={styles.currency__title}>
|
|
<span>{displayedAmount}</span> {title}
|
|
</p>
|
|
{showTooltip && (
|
|
<Tooltip
|
|
className={styles.currency__check__tooltip}
|
|
shown={textHovered}
|
|
>
|
|
{displayedAmount}
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const assets = state.wallet?.connected ? state.wallet?.assets || [] : [];
|
|
|
|
const openable = assets.length > 1;
|
|
|
|
return (
|
|
<div className={styles.header__currency__wrapper}>
|
|
<div
|
|
onClick={(e) => {
|
|
if (e.target === eyeRef.current || !openable) return;
|
|
setCurrencyState(!currencyCheckOpended);
|
|
}}
|
|
className={`${styles.header__currency__check} ${
|
|
currencyCheckOpended ? styles.currency__check__opened : ''
|
|
} ${openable ? styles.currency__check__openable : ''}`}
|
|
>
|
|
<Row
|
|
balanceSeen={balanceSeen}
|
|
icon={letheanIcon}
|
|
title="LTHN"
|
|
amount={Number(assets.find((e) => e.ticker === 'LTHN')?.balance) || 0}
|
|
></Row>
|
|
{/* <img
|
|
onClick={() => setBalanceState(!balanceSeen)}
|
|
src={balanceSeen ? eyeCloseIcon : eyeIcon} alt="see"
|
|
style={!balanceSeen ? {opacity: "0.6"} : {}}
|
|
/> */}
|
|
<div
|
|
onClick={() => setBalanceState(!balanceSeen)}
|
|
style={!balanceSeen ? { opacity: '0.6' } : {}}
|
|
ref={eyeRef}
|
|
>
|
|
{balanceSeen ? (
|
|
<EyeCloseIcon className="filled" />
|
|
) : (
|
|
<EyeIcon className="filled" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
{currencyCheckOpended && (
|
|
<div className={styles.currency__check__menu}>
|
|
{assets
|
|
?.filter((e) => e.ticker !== 'LTHN')
|
|
.map((e) => (
|
|
<Row
|
|
key={e.ticker}
|
|
balanceSeen={balanceSeen}
|
|
icon={getIcon(e.ticker)}
|
|
title={e.ticker}
|
|
amount={Number(e.balance)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ThemeButton(props: { className?: string }) {
|
|
const [tooltipShown, setTooltipState] = useState(false);
|
|
|
|
return (
|
|
<div className={styles.theme__btn__wrapper}>
|
|
<Button
|
|
onMouseEnter={() => setTooltipState(true)}
|
|
onMouseLeave={() => setTooltipState(false)}
|
|
className={`${styles.theme__btn} ${props.className}`}
|
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
transparent
|
|
>
|
|
<img src={theme === 'dark' ? sunIcon : moonIcon} alt="theme" />
|
|
</Button>
|
|
<Tooltip className={styles.theme__tooltip} shown={tooltipShown}>
|
|
{theme === 'dark' ? 'Light' : 'Dark'} Theme
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.header__menu} ${props.isMobile ? styles.header__menu__mobile : ''}`}
|
|
>
|
|
<>
|
|
{loggedIn ? (
|
|
<div className={styles.header__account}>
|
|
{CurrencyCheck()}
|
|
<div className={styles.header__account__wrapper}>
|
|
<div className={styles.header__account__info}>
|
|
<p>
|
|
{state.wallet?.connected && state.wallet.alias
|
|
? `@${state.wallet.alias}`
|
|
: '@no alias'}
|
|
</p>
|
|
<p>
|
|
{state.wallet?.connected
|
|
? shortenAddress(state.wallet.address || '')
|
|
: '...'}
|
|
</p>
|
|
</div>
|
|
<div className={styles.header__account__panel}>
|
|
<div>
|
|
<ThemeButton className={styles.header__account__btn} />
|
|
</div>
|
|
|
|
<Button
|
|
onClick={logout}
|
|
className={styles.header__account__btn}
|
|
transparent={true}
|
|
>
|
|
<LogOutIcon className="stroked" />
|
|
</Button>
|
|
|
|
<BurgerButton />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={styles.header__login}>
|
|
<ThemeButton />
|
|
<ConnectButton autoAuth className={styles.header__connect_btn} />
|
|
{!props.isMobile && <BurgerButton />}
|
|
</div>
|
|
)}
|
|
{props.isMobile && <NavBar mobile />}
|
|
</>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
useEffect(() => {
|
|
const token = sessionStorage.getItem('token');
|
|
|
|
if (token) {
|
|
socket.emit('in-dex-notifications', { token });
|
|
|
|
return () => {
|
|
socket.emit('out-dex-notifications', { token });
|
|
};
|
|
}
|
|
}, [state.user?.address]);
|
|
|
|
const [activeNotifications, setActiveNotifications] = useState(
|
|
new Map<number, { Notification: Notification; orderData: OrderDataWithPair }>(),
|
|
);
|
|
|
|
const fetchUser = useUpdateUser();
|
|
|
|
useEffect(() => {
|
|
function onNotify({ orderData }: { orderData: OrderDataWithPair }) {
|
|
if (state.user) {
|
|
fetchUser();
|
|
if (!!('Notification' in window) && Notification.permission === 'granted') {
|
|
const { pair } = orderData;
|
|
|
|
const pairLink = `/dex/trading/${pair.id}#my_orders`;
|
|
|
|
const notification = new Notification('Lethean Trade - New offer', {
|
|
body: `You have new offer: ${orderData.type === 'buy' ? 'Buy' : 'Sell'} | ${
|
|
pair.first_currency.name
|
|
}/${pair.second_currency.name} | Price: ${notationToString(
|
|
orderData.price,
|
|
)}`,
|
|
icon: letheanImg,
|
|
});
|
|
|
|
notification.onclick = () => {
|
|
router.push(pairLink);
|
|
};
|
|
|
|
setActiveNotifications((prevNotifications) => {
|
|
const newNotifications = new Map(prevNotifications);
|
|
console.log('Added notification with orderId:', orderData.id);
|
|
|
|
newNotifications.set(parseInt(orderData.id, 10), {
|
|
Notification: notification,
|
|
orderData,
|
|
});
|
|
return newNotifications;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function onCancel({ orderId }: { orderId: number }) {
|
|
fetchUser();
|
|
setActiveNotifications((prevNotifications) => {
|
|
const newNotifications = new Map(prevNotifications);
|
|
const notification = newNotifications.get(orderId);
|
|
|
|
if (notification) {
|
|
notification.Notification.close();
|
|
newNotifications.delete(orderId);
|
|
console.log('Deleted notification with orderId:', orderId);
|
|
}
|
|
|
|
return newNotifications;
|
|
});
|
|
}
|
|
|
|
if (state.closed_notifications.length > 0) {
|
|
for (const notificationID of state.closed_notifications) {
|
|
onCancel({ orderId: notificationID });
|
|
}
|
|
|
|
console.log('Closed notifications:', state.closed_notifications);
|
|
|
|
updateAutoClosedNotification(dispatch, []);
|
|
}
|
|
|
|
socket.on('order-notification', onNotify);
|
|
socket.on('order-notification-cancelation', onCancel);
|
|
|
|
return () => {
|
|
socket.off('order-notification', onNotify);
|
|
socket.off('order-notification-cancelation', onCancel);
|
|
};
|
|
}, [state.user, activeNotifications, state.closed_notifications]);
|
|
|
|
const mobileHeaderStyle: React.CSSProperties = {};
|
|
|
|
if (!menuOpened) {
|
|
mobileHeaderStyle.maxHeight = '0';
|
|
} else if (currencyCheckOpended) {
|
|
mobileHeaderStyle.overflow = 'inherit';
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{menuOpened && <div className={styles.header__blur__block}></div>}
|
|
<header className={classes(styles.header, isLg && styles.lg)}>
|
|
<div className={styles.header__logo}>
|
|
<Link href="/dex">
|
|
<img src={theme === 'dark' ? logoImg : logoImgWhite} alt="Lethean P2P" />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className={styles.header__desktop__navigation}>
|
|
<NavBar isLg={isLg} />
|
|
</div>
|
|
|
|
{Menu()}
|
|
|
|
<BurgerButton className={styles.header__burger} />
|
|
|
|
<div className={styles.header__account__mobile} style={mobileHeaderStyle}>
|
|
<div>
|
|
<Menu isMobile={true} />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Header;
|