mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-26 22:06:08 +03:00
Refactor contexts and models (#921)
* Add SVG icons for map pins * Add federation basis and new coordinator form (#793) * Add new coordinator entry issue form * Add Federation basis * Fix eslint errors from F2F and fix languages * Redo eslint @typescript-eslint/strict-boolean-expressions * Robot Page working * Contexts Working * Garage Working * CurrentOrder working * Federation model working --------- Co-authored-by: Reckless_Satoshi <reckless.satoshi@protonmail.com> Co-authored-by: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com>
This commit is contained in:
parent
f6b0d34264
commit
293c0b604d
8
.github/workflows/js-linter.yml
vendored
8
.github/workflows/js-linter.yml
vendored
@ -8,7 +8,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- frontend
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
@ -41,7 +41,5 @@ jobs:
|
||||
with:
|
||||
prettier: true
|
||||
prettier_dir: frontend
|
||||
|
||||
## Disabled due to error
|
||||
# eslint: true
|
||||
# eslint_dir: frontend
|
||||
eslint: true
|
||||
eslint_dir: frontend
|
@ -61,6 +61,7 @@ services:
|
||||
volumes:
|
||||
- ./nodeapp/:/usr/src/robosats/
|
||||
- ./nodeapp/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./nodeapp/coordinators/:/etc/nginx/conf.d/
|
||||
- ./frontend/static:/usr/src/robosats/static
|
||||
|
||||
clean-orders:
|
||||
|
@ -92,9 +92,15 @@ Each RoboSats coordinator has a profile in the RoboSats app. The profile contain
|
||||
- Large Limits - Can host orders with large limits.
|
||||
- DevFund donator - Donates to the DevFund the default amount or more.
|
||||
|
||||
<<<<<<< HEAD
|
||||
Some of these badges can be objectively measured and awarded. Other badges rely on the subjectivity of the development team. These will be generously awarded and only taken away after a warning.
|
||||
|
||||
We also envision more badges in the future, for example milestones by number of trades coordinated (200, 1K, 5K, 25K, 100K, etc).
|
||||
=======
|
||||
Some of these badges can be objectively measured and therefore awarded. Others badges rely on the subjectivity by the development team. These will be generously awarded and only taken away after a warning.
|
||||
|
||||
We also envision more badges in the future. For example milestones of number of trades coordinated (200, 1K, 5K, 25K, 100K, etc).
|
||||
>>>>>>> 2254fa60 (Add federation basis and new coordinator form (#793))
|
||||
|
||||
## Timeline
|
||||
|
||||
|
@ -19,12 +19,21 @@
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"ignorePatterns": ["index.js", "**/PaymentMethods/Icons/code/code.js"],
|
||||
"plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "variableLike",
|
||||
"format": ["camelCase", "snake_case", "PascalCase", "UPPER_CASE"],
|
||||
"leadingUnderscore": "allow"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { StrictMode, Suspense } from 'react';
|
||||
import React, { StrictMode, Suspense, useState } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Main from './basic/Main';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
@ -11,20 +11,29 @@ import i18n from './i18n/Web';
|
||||
|
||||
import { systemClient } from './services/System';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import { GarageContext, useGarageStore } from './contexts/GarageContext';
|
||||
import { FederationContext, useFederationStore } from './contexts/FederationContext';
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
const store = useAppStore();
|
||||
const appStore = useAppStore();
|
||||
const garageStore = useGarageStore();
|
||||
const federationStore = useFederationStore();
|
||||
|
||||
return (
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback='loading'>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppContext.Provider value={store}>
|
||||
<ThemeProvider theme={store.theme}>
|
||||
<AppContext.Provider value={appStore}>
|
||||
<GarageContext.Provider value={garageStore}>
|
||||
<FederationContext.Provider value={federationStore}>
|
||||
<ThemeProvider theme={appStore.theme}>
|
||||
<CssBaseline />
|
||||
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
</FederationContext.Provider>
|
||||
</GarageContext.Provider>
|
||||
</AppContext.Provider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
@ -33,7 +42,7 @@ const App = (): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const loadApp = () => {
|
||||
const loadApp = (): void => {
|
||||
// waits until the environment is ready for the Android WebView app
|
||||
if (systemClient.loading) {
|
||||
setTimeout(loadApp, 200);
|
||||
|
@ -12,10 +12,13 @@ import BookTable from '../../components/BookTable';
|
||||
import { BarChart, FormatListBulleted, Map } from '@mui/icons-material';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import MapChart from '../../components/Charts/MapChart';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const BookPage = (): JSX.Element => {
|
||||
const { robot, fetchBook, windowSize, setDelay, setOrder } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { windowSize } = useContext<UseAppStoreType>(AppContext);
|
||||
const { setDelay } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, clearOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [view, setView] = useState<'list' | 'depth' | 'map'>('list');
|
||||
@ -27,25 +30,17 @@ const BookPage = (): JSX.Element => {
|
||||
const maxBookTableWidth = 85;
|
||||
const chartWidthEm = width - maxBookTableWidth;
|
||||
|
||||
useEffect(() => {
|
||||
fetchBook();
|
||||
}, []);
|
||||
|
||||
const onViewOrder = function () {
|
||||
setOrder(undefined);
|
||||
const onOrderClicked = function (id: number, shortAlias: string): void {
|
||||
if (garage.getRobot().avatarLoaded) {
|
||||
clearOrder();
|
||||
setDelay(10000);
|
||||
};
|
||||
|
||||
const onOrderClicked = function (id: number) {
|
||||
if (robot.avatarLoaded) {
|
||||
navigate('/order/' + id);
|
||||
onViewOrder();
|
||||
navigate(`/order/${shortAlias}/${id}`);
|
||||
} else {
|
||||
setOpenNoRobot(true);
|
||||
}
|
||||
};
|
||||
|
||||
const NavButtons = function () {
|
||||
const NavButtons = function (): JSX.Element {
|
||||
return (
|
||||
<ButtonGroup variant='contained' color='inherit'>
|
||||
<Button
|
||||
@ -60,13 +55,25 @@ const BookPage = (): JSX.Element => {
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={() => setView('list')}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setView('list');
|
||||
}}
|
||||
>
|
||||
<FormatListBulleted /> {t('List')}
|
||||
</Button>
|
||||
<Button onClick={() => setView('depth')}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setView('depth');
|
||||
}}
|
||||
>
|
||||
<BarChart /> {t('Chart')}
|
||||
</Button>
|
||||
<Button onClick={() => setView('map')}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setView('map');
|
||||
}}
|
||||
>
|
||||
<Map /> {t('Map')}
|
||||
</Button>
|
||||
</>
|
||||
@ -82,7 +89,9 @@ const BookPage = (): JSX.Element => {
|
||||
onClose={() => {
|
||||
setOpenNoRobot(false);
|
||||
}}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
{openMaker ? (
|
||||
<Dialog
|
||||
@ -94,9 +103,11 @@ const BookPage = (): JSX.Element => {
|
||||
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
||||
<MakerForm
|
||||
onOrderCreated={(id) => {
|
||||
navigate('/order/' + id);
|
||||
navigate(`/order/${id}`);
|
||||
}}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
|
@ -1,20 +1,15 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { Box, Slide, Typography, styled } from '@mui/material';
|
||||
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
|
||||
|
||||
import RobotPage from './RobotPage';
|
||||
import MakerPage from './MakerPage';
|
||||
import BookPage from './BookPage';
|
||||
import OrderPage from './OrderPage';
|
||||
import SettingsPage from './SettingsPage';
|
||||
import NavBar from './NavBar';
|
||||
import MainDialogs from './MainDialogs';
|
||||
|
||||
import { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage, NavBar, MainDialogs } from './';
|
||||
import RobotAvatar from '../components/RobotAvatar';
|
||||
import Notifications from '../components/Notifications';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Notifications from '../components/Notifications';
|
||||
import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../contexts/GarageContext';
|
||||
|
||||
const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter;
|
||||
|
||||
@ -35,39 +30,31 @@ const MainBox = styled(Box)<MainBoxProps>((props) => ({
|
||||
|
||||
const Main: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
settings,
|
||||
robot,
|
||||
setRobot,
|
||||
baseUrl,
|
||||
order,
|
||||
page,
|
||||
slideDirection,
|
||||
setOpen,
|
||||
windowSize,
|
||||
navbarHeight,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
const { settings, page, slideDirection, setOpen, windowSize, navbarHeight, hostUrl, origin } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const [avatarBaseUrl, setAvatarBaseUrl] = useState<string>(hostUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl());
|
||||
}, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<RobotAvatar
|
||||
style={{ display: 'none' }}
|
||||
nickname={robot.nickname}
|
||||
baseUrl={baseUrl}
|
||||
onLoad={() => {
|
||||
setRobot((robot) => {
|
||||
return { ...robot, avatarLoaded: true };
|
||||
});
|
||||
}}
|
||||
nickname={garage.getRobot().nickname}
|
||||
baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()}
|
||||
onLoad={() => garage.updateRobot({ avatarLoaded: true })}
|
||||
/>
|
||||
<Notifications
|
||||
order={order}
|
||||
page={page}
|
||||
openProfile={() => {
|
||||
setOpen({ ...closeAll, profile: true });
|
||||
}}
|
||||
rewards={robot.earnedRewards}
|
||||
windowWidth={windowSize.width}
|
||||
rewards={garage.getRobot().earnedRewards}
|
||||
windowWidth={windowSize?.width}
|
||||
/>
|
||||
{settings.network === 'testnet' ? (
|
||||
<TestnetTypography color='secondary' align='center'>
|
||||
@ -87,10 +74,10 @@ const Main: React.FC = () => {
|
||||
<Slide
|
||||
direction={page === 'robot' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'robot'}
|
||||
appear={slideDirection.in != undefined}
|
||||
appear={slideDirection.in !== undefined}
|
||||
>
|
||||
<div>
|
||||
<RobotPage />
|
||||
<RobotPage avatarBaseUrl={avatarBaseUrl} />
|
||||
</div>
|
||||
</Slide>
|
||||
}
|
||||
@ -105,7 +92,7 @@ const Main: React.FC = () => {
|
||||
<Slide
|
||||
direction={page === 'offers' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'offers'}
|
||||
appear={slideDirection.in != undefined}
|
||||
appear={slideDirection.in !== undefined}
|
||||
>
|
||||
<div>
|
||||
<BookPage />
|
||||
@ -120,7 +107,7 @@ const Main: React.FC = () => {
|
||||
<Slide
|
||||
direction={page === 'create' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'create'}
|
||||
appear={slideDirection.in != undefined}
|
||||
appear={slideDirection.in !== undefined}
|
||||
>
|
||||
<div>
|
||||
<MakerPage />
|
||||
@ -130,12 +117,12 @@ const Main: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path='/order/:orderId'
|
||||
path='/order/:shortAlias/:orderId'
|
||||
element={
|
||||
<Slide
|
||||
direction={page === 'order' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'order'}
|
||||
appear={slideDirection.in != undefined}
|
||||
appear={slideDirection.in !== undefined}
|
||||
>
|
||||
<div>
|
||||
<OrderPage />
|
||||
@ -150,7 +137,7 @@ const Main: React.FC = () => {
|
||||
<Slide
|
||||
direction={page === 'settings' ? slideDirection.in : slideDirection.out}
|
||||
in={page === 'settings'}
|
||||
appear={slideDirection.in != undefined}
|
||||
appear={slideDirection.in !== undefined}
|
||||
>
|
||||
<div>
|
||||
<SettingsPage />
|
||||
@ -160,7 +147,7 @@ const Main: React.FC = () => {
|
||||
/>
|
||||
</Routes>
|
||||
</MainBox>
|
||||
<NavBar width={windowSize.width} height={navbarHeight} />
|
||||
<NavBar />
|
||||
<MainDialogs />
|
||||
</Router>
|
||||
);
|
||||
|
@ -1,16 +1,17 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import {
|
||||
CommunityDialog,
|
||||
CoordinatorSummaryDialog,
|
||||
InfoDialog,
|
||||
ExchangeDialog,
|
||||
CoordinatorDialog,
|
||||
AboutDialog,
|
||||
LearnDialog,
|
||||
ProfileDialog,
|
||||
StatsDialog,
|
||||
UpdateClientDialog,
|
||||
NoticeDialog,
|
||||
ClientDialog,
|
||||
UpdateDialog,
|
||||
} from '../../components/Dialogs';
|
||||
import { pn } from '../../utils';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
export interface OpenDialogs {
|
||||
more: boolean;
|
||||
@ -18,100 +19,96 @@ export interface OpenDialogs {
|
||||
community: boolean;
|
||||
info: boolean;
|
||||
coordinator: boolean;
|
||||
stats: boolean;
|
||||
exchange: boolean;
|
||||
client: boolean;
|
||||
update: boolean;
|
||||
profile: boolean;
|
||||
notice: boolean;
|
||||
}
|
||||
|
||||
const MainDialogs = (): JSX.Element => {
|
||||
const { open, setOpen, info, limits, robot, setRobot, setCurrentOrder, baseUrl } =
|
||||
const { open, setOpen, settings, clientVersion, hostUrl } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, focusedCoordinator, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [maxAmount, setMaxAmount] = useState<string>('...loading...');
|
||||
|
||||
useEffect(() => {
|
||||
if (limits.list[1000]) {
|
||||
setMaxAmount(pn(limits.list[1000].max_amount * 100000000));
|
||||
if (focusedCoordinator) {
|
||||
const limits = federation.getCoordinator(focusedCoordinator).limits;
|
||||
if (limits[1000] !== undefined) {
|
||||
setMaxAmount(pn(limits[1000].max_amount * 100000000));
|
||||
}
|
||||
}, [limits.list]);
|
||||
|
||||
useEffect(() => {
|
||||
if (info.openUpdateClient) {
|
||||
setOpen((open) => {
|
||||
return { ...open, update: true };
|
||||
});
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!info.loading && info.notice_severity !== 'none' && info.notice_message !== '') {
|
||||
setOpen((open) => {
|
||||
return { ...open, notice: true };
|
||||
});
|
||||
}
|
||||
}, [info]);
|
||||
}, [coordinatorUpdatedAt]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateClientDialog
|
||||
open={open.update}
|
||||
coordinatorVersion={info.coordinatorVersion}
|
||||
clientVersion={info.clientVersion}
|
||||
<UpdateDialog
|
||||
coordinatorVersion={federation.exchange.info.version}
|
||||
clientVersion={clientVersion.semver}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, update: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, update: false };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<NoticeDialog
|
||||
open={open.notice}
|
||||
severity={info.notice_severity}
|
||||
message={info.notice_message}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, notice: false });
|
||||
}}
|
||||
/>
|
||||
<InfoDialog
|
||||
<AboutDialog
|
||||
open={open.info}
|
||||
maxAmount={maxAmount}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, info: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, info: false };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LearnDialog
|
||||
open={open.learn}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, learn: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, learn: false };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CommunityDialog
|
||||
open={open.community}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, community: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, community: false };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CoordinatorSummaryDialog
|
||||
open={open.coordinator}
|
||||
<ExchangeDialog
|
||||
open={open.exchange}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, coordinator: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, exchange: false };
|
||||
});
|
||||
}}
|
||||
info={info}
|
||||
/>
|
||||
<StatsDialog
|
||||
open={open.stats}
|
||||
<ClientDialog
|
||||
open={open.client}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, stats: false });
|
||||
setOpen((open) => {
|
||||
return { ...open, client: false };
|
||||
});
|
||||
}}
|
||||
info={info}
|
||||
/>
|
||||
<ProfileDialog
|
||||
open={open.profile}
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={hostUrl}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, profile: false });
|
||||
}}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
/>
|
||||
<CoordinatorDialog
|
||||
open={open.coordinator}
|
||||
network={settings.network}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
coordinator={focusedCoordinator ? federation.getCoordinator(focusedCoordinator) : null}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -2,7 +2,6 @@ import React, { useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Grid, Paper, Collapse, Typography } from '@mui/material';
|
||||
|
||||
import { filterOrders } from '../../utils';
|
||||
|
||||
import MakerForm from '../../components/MakerForm';
|
||||
@ -10,10 +9,13 @@ import BookTable from '../../components/BookTable';
|
||||
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { NoRobotDialog } from '../../components/Dialogs';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const MakerPage = (): JSX.Element => {
|
||||
const { robot, book, fav, maker, windowSize, navbarHeight, setOrder, setDelay } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
|
||||
const { setDelay, federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -23,7 +25,7 @@ const MakerPage = (): JSX.Element => {
|
||||
|
||||
const matches = useMemo(() => {
|
||||
return filterOrders({
|
||||
orders: book.orders,
|
||||
orders: federation.book,
|
||||
baseFilter: {
|
||||
currency: fav.currency === 0 ? 1 : fav.currency,
|
||||
type: fav.type,
|
||||
@ -39,7 +41,7 @@ const MakerPage = (): JSX.Element => {
|
||||
},
|
||||
});
|
||||
}, [
|
||||
book.orders,
|
||||
federation.book,
|
||||
fav,
|
||||
maker.premium,
|
||||
maker.amount,
|
||||
@ -48,14 +50,14 @@ const MakerPage = (): JSX.Element => {
|
||||
maker.paymentMethods,
|
||||
]);
|
||||
|
||||
const onViewOrder = function () {
|
||||
setOrder(undefined);
|
||||
const onViewOrder = function (): void {
|
||||
garage.updateOrder(null);
|
||||
setDelay(10000);
|
||||
};
|
||||
|
||||
const onOrderClicked = function (id: number) {
|
||||
if (robot.avatarLoaded) {
|
||||
navigate('/order/' + id);
|
||||
const onOrderClicked = function (id: number): void {
|
||||
if (garage.getRobot().avatarLoaded) {
|
||||
navigate(`/order/${id}`);
|
||||
onViewOrder();
|
||||
} else {
|
||||
setOpenNoRobot(true);
|
||||
@ -69,7 +71,9 @@ const MakerPage = (): JSX.Element => {
|
||||
onClose={() => {
|
||||
setOpenNoRobot(false);
|
||||
}}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
<Grid item>
|
||||
<Collapse in={matches.length > 0 && showMatches}>
|
||||
@ -103,8 +107,8 @@ const MakerPage = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<MakerForm
|
||||
onOrderCreated={(id) => {
|
||||
navigate('/order/' + id);
|
||||
onOrderCreated={(shortAlias, id) => {
|
||||
navigate(`/order/${shortAlias}/${id}`);
|
||||
}}
|
||||
disableRequest={matches.length > 0 && !showMatches}
|
||||
collapseAll={showMatches}
|
||||
@ -115,7 +119,9 @@ const MakerPage = (): JSX.Element => {
|
||||
setShowMatches(false);
|
||||
}}
|
||||
submitButtonLabel={matches.length > 0 && !showMatches ? 'Submit' : 'Create order'}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme, styled, Grid, IconButton } from '@mui/material';
|
||||
import Tooltip, { type TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
||||
import { closeAll } from '../../contexts/AppContext';
|
||||
import { type OpenDialogs } from '../MainDialogs';
|
||||
import { closeAll, type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
|
||||
import { BubbleChart, Info, People, PriceChange, School } from '@mui/icons-material';
|
||||
|
||||
@ -20,13 +19,13 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
}));
|
||||
|
||||
interface MoreTooltipProps {
|
||||
open: OpenDialogs;
|
||||
setOpen: (state: OpenDialogs) => void;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element => {
|
||||
const MoreTooltip = ({ children }: MoreTooltipProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { open, setOpen } = useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledTooltip
|
||||
@ -89,15 +88,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Coordinator summary')}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Exchange summary')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.coordinator
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.text.secondary,
|
||||
color: open.exchange ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen({ ...closeAll, coordinator: !open.coordinator });
|
||||
setOpen({ ...closeAll, exchange: !open.exchange });
|
||||
}}
|
||||
>
|
||||
<PriceChange />
|
||||
@ -106,13 +103,13 @@ const MoreTooltip = ({ open, setOpen, children }: MoreTooltipProps): JSX.Element
|
||||
</Grid>
|
||||
|
||||
<Grid item sx={{ position: 'relative', right: '0.4em' }}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('Stats for nerds')}>
|
||||
<Tooltip enterTouchDelay={250} placement='left' title={t('client for nerds')}>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: open.stats ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
color: open.client ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen({ ...closeAll, stats: !open.stats });
|
||||
setOpen({ ...closeAll, client: !open.client });
|
||||
}}
|
||||
>
|
||||
<BubbleChart />
|
||||
|
@ -4,7 +4,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Tabs, Tab, Paper, useTheme } from '@mui/material';
|
||||
import MoreTooltip from './MoreTooltip';
|
||||
|
||||
import { type Page } from '.';
|
||||
import { type Page, isPage } from '.';
|
||||
|
||||
import {
|
||||
SettingsApplications,
|
||||
@ -16,33 +16,37 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface NavBarProps {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
const NavBar = (): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
robot,
|
||||
page,
|
||||
settings,
|
||||
setPage,
|
||||
settings,
|
||||
setSlideDirection,
|
||||
open,
|
||||
setOpen,
|
||||
currentOrder,
|
||||
baseUrl,
|
||||
windowSize,
|
||||
navbarHeight,
|
||||
hostUrl,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const smallBar = width < 50;
|
||||
const smallBar = windowSize?.width < 50;
|
||||
const color = settings.network === 'mainnet' ? 'primary' : 'secondary';
|
||||
|
||||
const tabSx = smallBar
|
||||
? { position: 'relative', bottom: robot.avatarLoaded ? '0.9em' : '0.13em', minWidth: '1em' }
|
||||
? {
|
||||
position: 'relative',
|
||||
bottom: garage.getRobot().avatarLoaded ? '0.9em' : '0.13em',
|
||||
minWidth: '1em',
|
||||
}
|
||||
: { position: 'relative', bottom: '1em', minWidth: '2em' };
|
||||
|
||||
const pagesPosition = {
|
||||
@ -55,17 +59,17 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
// change tab (page) into the current route
|
||||
const pathPage: Page = location.pathname.split('/')[1];
|
||||
if (pathPage == 'index.html') {
|
||||
const pathPage: Page | string = location.pathname.split('/')[1];
|
||||
if (pathPage === 'index.html') {
|
||||
navigate('/robot');
|
||||
setPage('robot');
|
||||
}
|
||||
if (pathPage) {
|
||||
if (isPage(pathPage)) {
|
||||
setPage(pathPage);
|
||||
}
|
||||
}, [location]);
|
||||
}, [location, navigate, setPage]);
|
||||
|
||||
const handleSlideDirection = function (oldPage: Page, newPage: Page) {
|
||||
const handleSlideDirection = function (oldPage: Page, newPage: Page): void {
|
||||
const oldPos: number = pagesPosition[oldPage];
|
||||
const newPos: number = pagesPosition[newPage];
|
||||
setSlideDirection(
|
||||
@ -73,13 +77,14 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const changePage = function (mouseEvent: any, newPage: Page) {
|
||||
const changePage = function (mouseEvent: any, newPage: Page): void {
|
||||
if (newPage === 'none') {
|
||||
return null;
|
||||
return;
|
||||
} else {
|
||||
handleSlideDirection(page, newPage);
|
||||
setPage(newPage);
|
||||
const param = newPage === 'order' ? currentOrder ?? '' : '';
|
||||
const param =
|
||||
newPage === 'order' ? `${String(currentOrder.shortAlias)}/${String(currentOrder.id)}` : '';
|
||||
setTimeout(() => {
|
||||
navigate(`/${newPage}/${param}`);
|
||||
}, theme.transitions.duration.leavingScreen * 3);
|
||||
@ -88,35 +93,41 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(closeAll);
|
||||
}, [page]);
|
||||
}, [page, setOpen]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{ height: `${height}em`, width: `100%`, position: 'fixed', bottom: 0, borderRadius: 0 }}
|
||||
sx={{
|
||||
height: `${navbarHeight}em`,
|
||||
width: `100%`,
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
TabIndicatorProps={{ sx: { height: '0.3em', position: 'absolute', top: 0 } }}
|
||||
variant='fullWidth'
|
||||
value={page}
|
||||
indicatorColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
|
||||
textColor={settings.network === 'mainnet' ? 'primary' : 'secondary'}
|
||||
indicatorColor={color}
|
||||
textColor={color}
|
||||
onChange={changePage}
|
||||
>
|
||||
<Tab
|
||||
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
|
||||
value='none'
|
||||
disabled={robot.nickname === null}
|
||||
disabled={garage.getRobot().nickname === null}
|
||||
onClick={() => {
|
||||
setOpen({ ...closeAll, profile: !open.profile });
|
||||
}}
|
||||
icon={
|
||||
robot.nickname && robot.avatarLoaded ? (
|
||||
garage.getRobot().nickname && garage.getRobot().avatarLoaded ? (
|
||||
<RobotAvatar
|
||||
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
|
||||
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
|
||||
nickname={robot.nickname}
|
||||
baseUrl={baseUrl}
|
||||
nickname={garage.getRobot().nickname}
|
||||
baseUrl={hostUrl}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@ -150,7 +161,7 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Order')}
|
||||
value='order'
|
||||
disabled={!robot.avatarLoaded || currentOrder == undefined}
|
||||
disabled={!garage.getRobot().avatarLoaded || currentOrder.id == null}
|
||||
icon={<Assignment />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
@ -166,11 +177,13 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('More')}
|
||||
value='none'
|
||||
onClick={(e) => {
|
||||
open.more ? null : setOpen({ ...open, more: true });
|
||||
onClick={() => {
|
||||
setOpen((open) => {
|
||||
return { ...open, more: !open.more };
|
||||
});
|
||||
}}
|
||||
icon={
|
||||
<MoreTooltip open={open} setOpen={setOpen} closeAll={closeAll}>
|
||||
<MoreTooltip>
|
||||
<MoreHoriz />
|
||||
</MoreTooltip>
|
||||
}
|
||||
|
@ -2,3 +2,7 @@ import NavBar from './NavBar';
|
||||
|
||||
export type Page = 'robot' | 'order' | 'create' | 'offers' | 'settings' | 'none';
|
||||
export default NavBar;
|
||||
|
||||
export function isPage(page: string): page is Page {
|
||||
return ['robot', 'order', 'create', 'offers', 'settings', 'none'].includes(page);
|
||||
}
|
||||
|
@ -8,41 +8,54 @@ import OrderDetails from '../../components/OrderDetails';
|
||||
|
||||
import { apiClient } from '../../services/api';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const OrderPage = (): JSX.Element => {
|
||||
const {
|
||||
windowSize,
|
||||
info,
|
||||
order,
|
||||
robot,
|
||||
settings,
|
||||
setOrder,
|
||||
clearOrder,
|
||||
currentOrder,
|
||||
setCurrentOrder,
|
||||
badOrder,
|
||||
setBadOrder,
|
||||
baseUrl,
|
||||
navbarHeight,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
const { windowSize, setOpen, settings, navbarHeight, hostUrl, origin } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { setFocusedCoordinator, federation, currentOrder, setCurrentOrder, focusedCoordinator } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, badOrder, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
const doublePageWidth: number = 50;
|
||||
const maxHeight: number = (windowSize.height - navbarHeight) * 0.85 - 3;
|
||||
const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3;
|
||||
|
||||
const [tab, setTab] = useState<'order' | 'contract'>('contract');
|
||||
const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrder != params.orderId) {
|
||||
clearOrder();
|
||||
setCurrentOrder(Number(params.orderId));
|
||||
}
|
||||
}, [params.orderId]);
|
||||
const newOrder = {
|
||||
shortAlias: params.shortAlias ?? '',
|
||||
id: Number(params.orderId) ?? null,
|
||||
order: null,
|
||||
};
|
||||
|
||||
const renewOrder = function () {
|
||||
if (order != undefined) {
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(newOrder.shortAlias)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
setBaseUrl(`${url}${basePath}`);
|
||||
|
||||
if (currentOrder.id !== newOrder.id || currentOrder.shortAlias !== newOrder.shortAlias) {
|
||||
setCurrentOrder(newOrder);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const onClickCoordinator = function (): void {
|
||||
if (currentOrder.shortAlias) {
|
||||
setFocusedCoordinator(currentOrder.shortAlias);
|
||||
}
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: true };
|
||||
});
|
||||
};
|
||||
|
||||
const renewOrder = function (): void {
|
||||
const order = currentOrder.order;
|
||||
if (order !== null && focusedCoordinator) {
|
||||
const body = {
|
||||
type: order.type,
|
||||
currency: order.currency,
|
||||
@ -60,32 +73,38 @@ const OrderPage = (): JSX.Element => {
|
||||
latitude: order.latitude,
|
||||
longitude: order.longitude,
|
||||
};
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 })
|
||||
.post(url, '/api/make/', body, { tokenSHA256: garage.getRobot().tokenSHA256 })
|
||||
.then((data: any) => {
|
||||
if (data.bad_request) {
|
||||
if (data.bad_request !== undefined) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.id) {
|
||||
navigate('/order/' + data.id);
|
||||
} else if (data.id !== undefined) {
|
||||
navigate(`/order/${String(currentOrder.shortAlias)}/${String(data.id)}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setBadOrder('Request error');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startAgain = () => {
|
||||
const startAgain = (): void => {
|
||||
navigate('/robot');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{order == undefined && badOrder == undefined ? <CircularProgress /> : null}
|
||||
{badOrder != undefined ? (
|
||||
{currentOrder.order === null && badOrder === undefined && <CircularProgress />}
|
||||
{badOrder !== undefined ? (
|
||||
<Typography align='center' variant='subtitle2' color='secondary'>
|
||||
{t(badOrder)}
|
||||
</Typography>
|
||||
) : null}
|
||||
{order != undefined && badOrder == undefined ? (
|
||||
order.is_participant ? (
|
||||
{currentOrder.order !== null && badOrder === undefined ? (
|
||||
currentOrder.order.is_participant ? (
|
||||
windowSize.width > doublePageWidth ? (
|
||||
// DOUBLE PAPER VIEW
|
||||
<Grid
|
||||
@ -106,12 +125,12 @@ const OrderPage = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
|
||||
onClickCoordinator={onClickCoordinator}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
@ -125,10 +144,8 @@ const OrderPage = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<TradeBox
|
||||
order={order}
|
||||
robot={robot}
|
||||
robot={garage.getRobot()}
|
||||
settings={settings}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
@ -160,22 +177,20 @@ const OrderPage = (): JSX.Element => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: tab == 'order' ? '' : 'none' }}>
|
||||
<div style={{ display: tab === 'order' ? '' : 'none' }}>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
|
||||
onClickCoordinator={onClickCoordinator}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
|
||||
<div style={{ display: tab === 'contract' ? '' : 'none' }}>
|
||||
<TradeBox
|
||||
order={order}
|
||||
robot={robot}
|
||||
robot={garage.getRobot()}
|
||||
settings={settings}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
@ -195,12 +210,12 @@ const OrderPage = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
coordinator={federation.getCoordinator(String(currentOrder.shortAlias))}
|
||||
onClickCoordinator={onClickCoordinator}
|
||||
baseUrl={hostUrl}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
@ -21,6 +21,8 @@ import RobotAvatar from '../../components/RobotAvatar';
|
||||
import TokenInput from './TokenInput';
|
||||
import { genBase62Token } from '../../utils';
|
||||
import { NewTabIcon } from '../../components/Icons';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface OnboardingProps {
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
@ -35,22 +37,22 @@ interface OnboardingProps {
|
||||
|
||||
const Onboarding = ({
|
||||
setView,
|
||||
robot,
|
||||
inputToken,
|
||||
setInputToken,
|
||||
setRobot,
|
||||
badToken,
|
||||
getGenerateRobot,
|
||||
baseUrl,
|
||||
}: OnboardingProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const [step, setStep] = useState<'1' | '2' | '3'>('1');
|
||||
const [generatedToken, setGeneratedToken] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const generateToken = () => {
|
||||
const generateToken = (): void => {
|
||||
setGeneratedToken(true);
|
||||
setInputToken(genBase62Token(36));
|
||||
setLoading(true);
|
||||
@ -63,7 +65,7 @@ const Onboarding = ({
|
||||
<Box>
|
||||
<Accordion expanded={step === '1'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '1' ? 'text.primary' : 'text.disabled'}>
|
||||
<Typography variant='h5' color={step === '1' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('1. Generate a token')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
@ -101,9 +103,7 @@ const Onboarding = ({
|
||||
autoFocusTarget='copyButton'
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
</Grid>
|
||||
@ -122,7 +122,7 @@ const Onboarding = ({
|
||||
onClick={() => {
|
||||
setStep('2');
|
||||
getGenerateRobot(inputToken);
|
||||
setRobot({ ...robot, nickname: undefined });
|
||||
garage.updateRobot({ nickname: undefined });
|
||||
}}
|
||||
variant='contained'
|
||||
size='large'
|
||||
@ -141,7 +141,7 @@ const Onboarding = ({
|
||||
|
||||
<Accordion expanded={step === '2'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '2' ? 'text.primary' : 'text.disabled'}>
|
||||
<Typography variant='h5' color={step === '2' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('2. Meet your robot identity')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
@ -149,7 +149,7 @@ const Onboarding = ({
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname) ? (
|
||||
t('This is your trading avatar')
|
||||
) : (
|
||||
<>
|
||||
@ -162,7 +162,7 @@ const Onboarding = ({
|
||||
|
||||
<Grid item sx={{ width: '13.5em' }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
nickname={garage.getRobot().nickname}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
@ -174,11 +174,11 @@ const Onboarding = ({
|
||||
width: '12.4em',
|
||||
}}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={hostUrl}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname) ? (
|
||||
<Grid item>
|
||||
<Typography align='center'>{t('Hi! My name is')}</Typography>
|
||||
<Typography component='h5' variant='h5'>
|
||||
@ -197,7 +197,7 @@ const Onboarding = ({
|
||||
width: '1.5em',
|
||||
}}
|
||||
/>
|
||||
<b>{robot.nickname}</b>
|
||||
<b>{garage.getRobot().nickname}</b>
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
@ -210,7 +210,9 @@ const Onboarding = ({
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid item>
|
||||
<Collapse in={!!(robot.avatarLoaded && robot.nickname)}>
|
||||
<Collapse
|
||||
in={!!(garage.getRobot().avatarLoaded && Boolean(garage.getRobot().nickname))}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStep('3');
|
||||
@ -229,7 +231,7 @@ const Onboarding = ({
|
||||
|
||||
<Accordion expanded={step === '3'} disableGutters={true}>
|
||||
<AccordionSummary>
|
||||
<Typography variant='h5' color={step == '3' ? 'text.primary' : 'text.disabled'}>
|
||||
<Typography variant='h5' color={step === '3' ? 'text.primary' : 'text.disabled'}>
|
||||
{t('3. Browse or create an order')}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
@ -6,8 +6,6 @@ import TokenInput from './TokenInput';
|
||||
import Key from '@mui/icons-material/Key';
|
||||
|
||||
interface RecoveryProps {
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
inputToken: string;
|
||||
badToken: string;
|
||||
@ -16,8 +14,6 @@ interface RecoveryProps {
|
||||
}
|
||||
|
||||
const Recovery = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
badToken,
|
||||
setView,
|
||||
@ -26,7 +22,7 @@ const Recovery = ({
|
||||
}: RecoveryProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRecover = () => {
|
||||
const onClickRecover = (): void => {
|
||||
getGenerateRobot(inputToken);
|
||||
setView('profile');
|
||||
};
|
||||
@ -48,15 +44,18 @@ const Recovery = ({
|
||||
showCopy={false}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
label={t('Paste token here')}
|
||||
robot={robot}
|
||||
onPressEnter={onClickRecover}
|
||||
badToken={badToken}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button variant='contained' size='large' disabled={!!badToken} onClick={onClickRecover}>
|
||||
<Button
|
||||
variant='contained'
|
||||
size='large'
|
||||
disabled={Boolean(badToken)}
|
||||
onClick={onClickRecover}
|
||||
>
|
||||
<Key /> <div style={{ width: '0.5em' }} />
|
||||
{t('Recover')}
|
||||
</Button>
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Box,
|
||||
useTheme,
|
||||
Tooltip,
|
||||
type SelectChangeEvent,
|
||||
} from '@mui/material';
|
||||
import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
@ -20,6 +21,8 @@ import { type Slot, type Robot } from '../../models';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { genBase62Token } from '../../utils';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface RobotProfileProps {
|
||||
robot: Robot;
|
||||
@ -29,23 +32,22 @@ interface RobotProfileProps {
|
||||
inputToken: string;
|
||||
logoutRobot: () => void;
|
||||
setInputToken: (state: string) => void;
|
||||
baseUrl: string;
|
||||
width: number;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const RobotProfile = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
getGenerateRobot,
|
||||
setInputToken,
|
||||
logoutRobot,
|
||||
setView,
|
||||
baseUrl,
|
||||
width,
|
||||
}: RobotProfileProps): JSX.Element => {
|
||||
const { currentSlot, garage, setCurrentSlot, windowSize } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
@ -53,19 +55,18 @@ const RobotProfile = ({
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.nickname && robot.avatarLoaded) {
|
||||
if (garage.getRobot().nickname && garage.getRobot().avatarLoaded) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [robot]);
|
||||
}, [robotUpdatedAt]);
|
||||
|
||||
const handleAddRobot = () => {
|
||||
getGenerateRobot(genBase62Token(36), garage.slots.length);
|
||||
const handleAddRobot = (): void => {
|
||||
getGenerateRobot(genBase62Token(36));
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const handleChangeSlot = (e) => {
|
||||
const slot = e.target.value;
|
||||
getGenerateRobot(garage.slots[slot].robot.token, slot);
|
||||
const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => {
|
||||
garage.currentSlot = Number(e.target.value);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
@ -80,7 +81,7 @@ const RobotProfile = ({
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{garage.getRobot().avatarLoaded && garage.getRobot().nickname ? (
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
<div
|
||||
style={{
|
||||
@ -99,7 +100,7 @@ const RobotProfile = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<b>{robot.nickname}</b>
|
||||
<b>{garage.getRobot().nickname}</b>
|
||||
{width < 19 ? null : (
|
||||
<Bolt
|
||||
sx={{
|
||||
@ -121,7 +122,7 @@ const RobotProfile = ({
|
||||
|
||||
<Grid item sx={{ width: `13.5em` }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
nickname={garage.getRobot().nickname}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
@ -134,9 +135,9 @@ const RobotProfile = ({
|
||||
}}
|
||||
tooltip={t('This is your trading avatar')}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={hostUrl}
|
||||
/>
|
||||
{robot.found && !robot.lastOrderId ? (
|
||||
{garage.getRobot().found && Number(garage.getRobot().lastOrderId) > 0 ? (
|
||||
<Typography align='center' variant='h6'>
|
||||
{t('Welcome back!')}
|
||||
</Typography>
|
||||
@ -145,27 +146,31 @@ const RobotProfile = ({
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{robot.activeOrderId && robot.avatarLoaded && robot.nickname ? (
|
||||
{Boolean(garage.getRobot().activeOrderId) &&
|
||||
garage.getRobot().avatarLoaded &&
|
||||
Boolean(garage.getRobot().nickname) ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.activeOrderId}`);
|
||||
navigate(`/order/${String(currentOrder.shortAlias)}/${String(currentOrder.id)}`);
|
||||
}}
|
||||
>
|
||||
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
{t('Active order #{{orderID}}', { orderID: garage.getRobot().activeOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{robot.lastOrderId && robot.avatarLoaded && robot.nickname ? (
|
||||
{Boolean(garage.getRobot().lastOrderId) &&
|
||||
garage.getRobot().avatarLoaded &&
|
||||
Boolean(garage.getRobot().nickname) ? (
|
||||
<Grid item container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.lastOrderId}`);
|
||||
navigate(`/order/${String(currentOrder.shortAlias)}/${String(currentOrder.id)}`);
|
||||
}}
|
||||
>
|
||||
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
{t('Last order #{{orderID}}', { orderID: garage.getRobot().lastOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
@ -221,8 +226,6 @@ const RobotProfile = ({
|
||||
editable={false}
|
||||
label={t('Store your token safely')}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
</Grid>
|
||||
@ -246,7 +249,7 @@ const RobotProfile = ({
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={loading ? 'loading' : currentSlot}
|
||||
value={loading ? 'loading' : garage.currentSlot}
|
||||
onChange={handleChangeSlot}
|
||||
>
|
||||
{loading ? (
|
||||
@ -271,7 +274,7 @@ const RobotProfile = ({
|
||||
smooth={true}
|
||||
style={{ width: '2.6em', height: '2.6em' }}
|
||||
placeholderType='loading'
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={hostUrl}
|
||||
small={true}
|
||||
/>
|
||||
</Grid>
|
||||
@ -314,7 +317,7 @@ const RobotProfile = ({
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
garage.delete();
|
||||
setCurrentSlot(0);
|
||||
garage.currentSlot = 0;
|
||||
logoutRobot();
|
||||
setView('welcome');
|
||||
}}
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconButton, LinearProgress, TextField, Tooltip } from '@mui/material';
|
||||
import { type Robot } from '../../models';
|
||||
import { ContentCopy } from '@mui/icons-material';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
|
||||
|
||||
interface TokenInputProps {
|
||||
robot: Robot;
|
||||
editable?: boolean;
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
setRobot: (state: Robot) => void;
|
||||
inputToken: string;
|
||||
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
|
||||
onPressEnter: () => void;
|
||||
@ -21,11 +19,9 @@ interface TokenInputProps {
|
||||
}
|
||||
|
||||
const TokenInput = ({
|
||||
robot,
|
||||
editable = true,
|
||||
showCopy = true,
|
||||
label,
|
||||
setRobot,
|
||||
fullWidth = true,
|
||||
onPressEnter,
|
||||
autoFocusTarget = 'textfield',
|
||||
@ -35,6 +31,7 @@ const TokenInput = ({
|
||||
setInputToken,
|
||||
}: TokenInputProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const [showCopied, setShowCopied] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,12 +43,12 @@ const TokenInput = ({
|
||||
} else {
|
||||
return (
|
||||
<TextField
|
||||
error={inputToken.length > 20 ? !!badToken : false}
|
||||
error={inputToken.length > 20 ? Boolean(badToken) : false}
|
||||
disabled={!editable}
|
||||
required={true}
|
||||
label={label || undefined}
|
||||
label={label ?? ''}
|
||||
value={inputToken}
|
||||
autoFocus={autoFocusTarget == 'textfield'}
|
||||
autoFocus={autoFocusTarget === 'textfield'}
|
||||
fullWidth={fullWidth}
|
||||
sx={{ borderColor: 'primary' }}
|
||||
variant={editable ? 'outlined' : 'filled'}
|
||||
@ -69,17 +66,15 @@ const TokenInput = ({
|
||||
endAdornment: showCopy ? (
|
||||
<Tooltip open={showCopied} title={t('Copied!')}>
|
||||
<IconButton
|
||||
autoFocus={autoFocusTarget == 'copyButton'}
|
||||
color={robot.copiedToken ? 'inherit' : 'primary'}
|
||||
autoFocus={autoFocusTarget === 'copyButton'}
|
||||
color={garage.getRobot().copiedToken ? 'inherit' : 'primary'}
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(inputToken);
|
||||
setShowCopied(true);
|
||||
setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, 1000);
|
||||
setRobot((robot) => {
|
||||
return { ...robot, copiedToken: true };
|
||||
});
|
||||
garage.updateRobot({ copiedToken: true });
|
||||
}}
|
||||
>
|
||||
<ContentCopy sx={{ width: '1em', height: '1em' }} />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, Grid, Typography, useTheme } from '@mui/material';
|
||||
import { RoboSatsTextIcon } from '../../components/Icons';
|
||||
|
@ -21,13 +21,20 @@ import { TorIcon } from '../../components/Icons';
|
||||
import { genKey } from '../../pgp';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { validateTokenEntropy } from '../../utils';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const RobotPage = (): JSX.Element => {
|
||||
const { robot, setRobot, fetchRobot, torStatus, windowSize, baseUrl, settings } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
interface RobotPageProps {
|
||||
avatarBaseUrl: string;
|
||||
}
|
||||
|
||||
const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
|
||||
const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const url_token = settings.selfhostedClient ? params.token : null;
|
||||
const urlToken = settings.selfhostedClient ? params.token : null;
|
||||
const width = Math.min(windowSize.width * 0.8, 28);
|
||||
const maxHeight = windowSize.height * 0.85 - 3;
|
||||
const theme = useTheme();
|
||||
@ -35,21 +42,21 @@ const RobotPage = (): JSX.Element => {
|
||||
const [badToken, setBadToken] = useState<string>('');
|
||||
const [inputToken, setInputToken] = useState<string>('');
|
||||
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
|
||||
robot.token ? 'profile' : 'welcome',
|
||||
garage.getRobot().token !== undefined ? 'profile' : 'welcome',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.token) {
|
||||
setInputToken(robot.token);
|
||||
const token = urlToken ?? garage.getRobot().token;
|
||||
if (token !== undefined) {
|
||||
setInputToken(token);
|
||||
}
|
||||
const token = url_token ?? robot.token;
|
||||
if (robot.nickname == null && token) {
|
||||
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
|
||||
if (garage.getRobot().nickname !== undefined && token !== undefined) {
|
||||
if (window.NativeRobosats === undefined || torStatus === '"Done"') {
|
||||
getGenerateRobot(token);
|
||||
setView('profile');
|
||||
}
|
||||
}
|
||||
}, [torStatus]);
|
||||
}, [torStatus, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputToken.length < 20) {
|
||||
@ -61,26 +68,28 @@ const RobotPage = (): JSX.Element => {
|
||||
}
|
||||
}, [inputToken]);
|
||||
|
||||
const getGenerateRobot = (token: string, slot?: number) => {
|
||||
const getGenerateRobot = (token: string): void => {
|
||||
setInputToken(token);
|
||||
genKey(token).then(function (key) {
|
||||
fetchRobot({
|
||||
newKeys: {
|
||||
genKey(token)
|
||||
.then((key) => {
|
||||
const slot = garage.createRobot({
|
||||
token,
|
||||
pubKey: key.publicKeyArmored,
|
||||
encPrivKey: key.encryptedPrivateKeyArmored,
|
||||
},
|
||||
newToken: token,
|
||||
slot,
|
||||
});
|
||||
federation.fetchRobot(garage, slot);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
};
|
||||
|
||||
const logoutRobot = () => {
|
||||
const logoutRobot = (): void => {
|
||||
setInputToken('');
|
||||
setRobot(new Robot());
|
||||
garage.deleteSlot(garage.currentSlot);
|
||||
};
|
||||
|
||||
if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) {
|
||||
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={12}
|
||||
@ -146,35 +155,29 @@ const RobotPage = (): JSX.Element => {
|
||||
{view === 'onboarding' ? (
|
||||
<Onboarding
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
baseUrl={baseUrl}
|
||||
avatarBaseUrl={avatarBaseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'profile' ? (
|
||||
<RobotProfile
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
logoutRobot={logoutRobot}
|
||||
width={width}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
baseUrl={baseUrl}
|
||||
avatarBaseUrl={avatarBaseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'recovery' ? (
|
||||
<Recovery
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Grid, Paper } from '@mui/material';
|
||||
import SettingsForm from '../../components/SettingsForm';
|
||||
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import FederationTable from '../../components/FederationTable';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
const SettingsPage = (): JSX.Element => {
|
||||
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
|
||||
const { windowSize, navbarHeight, settings, setOpen, open, hostUrl } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
|
||||
|
||||
return (
|
||||
@ -12,7 +15,7 @@ const SettingsPage = (): JSX.Element => {
|
||||
elevation={12}
|
||||
sx={{
|
||||
padding: '0.6em',
|
||||
width: '21em',
|
||||
width: '20.5em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
overflowX: 'clip',
|
||||
@ -22,6 +25,16 @@ const SettingsPage = (): JSX.Element => {
|
||||
<Grid item>
|
||||
<SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FederationTable
|
||||
openCoordinator={() => {
|
||||
setOpen({ ...open, coordinator: true });
|
||||
}}
|
||||
baseUrl={hostUrl}
|
||||
maxHeight={14}
|
||||
network={settings.network}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
|
7
frontend/src/basic/index.ts
Normal file
7
frontend/src/basic/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { default as BookPage } from './BookPage';
|
||||
export { default as MainDialogs } from './MainDialogs';
|
||||
export { default as MakerPage } from './MakerPage';
|
||||
export { default as NavBar } from './NavBar';
|
||||
export { default as OrderPage } from './OrderPage';
|
||||
export { default as RobotPage } from './RobotPage';
|
||||
export { default as SettingsPage } from './SettingsPage';
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Typography,
|
||||
@ -26,7 +26,7 @@ interface BookControlProps {
|
||||
fav: Favorites;
|
||||
setFav: (state: Favorites) => void;
|
||||
paymentMethod: string[];
|
||||
setPaymentMethods: () => void;
|
||||
setPaymentMethods: (state: string[]) => void;
|
||||
}
|
||||
|
||||
const BookControl = ({
|
||||
@ -43,22 +43,22 @@ const BookControl = ({
|
||||
const typeZeroText = fav.mode === 'fiat' ? t('Buy') : t('Swap In');
|
||||
const typeOneText = fav.mode === 'fiat' ? t('Sell') : t('Swap Out');
|
||||
const small =
|
||||
(typeZeroText.length + typeOneText.length) * 0.7 + (fav.mode == 'fiat' ? 16 : 7.5);
|
||||
(typeZeroText.length + typeOneText.length) * 0.7 + (fav.mode === 'fiat' ? 16 : 7.5);
|
||||
const medium = small + 13;
|
||||
const large = medium + (t('and use').length + t('pay with').length) * 0.6 + 5;
|
||||
return [typeZeroText, typeOneText, small, medium, large];
|
||||
}, [i18n.language, fav.mode]);
|
||||
|
||||
const handleCurrencyChange = function (e) {
|
||||
const currency = e.target.value;
|
||||
const handleCurrencyChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const currency = Number(e.target.value);
|
||||
setFav({ ...fav, currency, mode: currency === 1000 ? 'swap' : 'fiat' });
|
||||
};
|
||||
|
||||
const handleTypeChange = function (mouseEvent, val) {
|
||||
const handleTypeChange = function (mouseEvent: React.MouseEvent, val: number): void {
|
||||
setFav({ ...fav, type: val });
|
||||
};
|
||||
|
||||
const handleModeChange = function (mouseEvent, val) {
|
||||
const handleModeChange = function (mouseEvent: React.MouseEvent, val: number): void {
|
||||
const mode = fav.mode === 'fiat' ? 'swap' : 'fiat';
|
||||
const currency = fav.mode === 'fiat' ? 1000 : 0;
|
||||
setFav({ ...fav, mode, currency });
|
||||
@ -197,7 +197,7 @@ const BookControl = ({
|
||||
{width > large ? (
|
||||
<Grid item sx={{ position: 'relative', top: '0.5em' }}>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{fav.currency == 1000 ? t(fav.type === 0 ? 'to' : 'from') : t('pay with')}
|
||||
{fav.currency === 1000 ? t(fav.type === 0 ? 'to' : 'from') : t('pay with')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
@ -218,10 +218,10 @@ const BookControl = ({
|
||||
listBoxProps={{ sx: { width: '13em' } }}
|
||||
onAutocompleteChange={setPaymentMethods}
|
||||
value={paymentMethod}
|
||||
optionsType={fav.currency == 1000 ? 'swap' : 'fiat'}
|
||||
optionsType={fav.currency === 1000 ? 'swap' : 'fiat'}
|
||||
error={false}
|
||||
helperText={''}
|
||||
label={fav.currency == 1000 ? t('DESTINATION') : t('METHOD')}
|
||||
label={fav.currency === 1000 ? t('DESTINATION') : t('METHOD')}
|
||||
tooltipTitle=''
|
||||
listHeaderText=''
|
||||
addNewButtonText=''
|
||||
@ -247,7 +247,7 @@ const BookControl = ({
|
||||
label={t('Select Payment Method')}
|
||||
required={true}
|
||||
renderValue={(value) =>
|
||||
value == 'ANY' ? (
|
||||
value === 'ANY' ? (
|
||||
<CheckBoxOutlineBlankIcon style={{ position: 'relative', top: '0.1em' }} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
@ -258,9 +258,9 @@ const BookControl = ({
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={paymentMethod[0] ? paymentMethod[0] : 'ANY'}
|
||||
value={paymentMethod[0] ?? 'ANY'}
|
||||
onChange={(e) => {
|
||||
setPaymentMethods(e.target.value == 'ANY' ? [] : [e.target.value]);
|
||||
setPaymentMethods(e.target.value === 'ANY' ? [] : [e.target.value]);
|
||||
}}
|
||||
>
|
||||
<MenuItem value={'ANY'}>
|
||||
|
@ -22,6 +22,8 @@ import {
|
||||
type GridColumnVisibilityModel,
|
||||
GridPagination,
|
||||
type GridPaginationModel,
|
||||
type GridColDef,
|
||||
type GridValidRowModel,
|
||||
} from '@mui/x-data-grid';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { type PublicOrder } from '../../models';
|
||||
@ -35,6 +37,7 @@ import RobotAvatar from '../RobotAvatar';
|
||||
// Icons
|
||||
import { Fullscreen, FullscreenExit, Refresh } from '@mui/icons-material';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
const ClickThroughDataGrid = styled(DataGrid)({
|
||||
'& .MuiDataGrid-overlayWrapperInner': {
|
||||
@ -62,15 +65,15 @@ const ClickThroughDataGrid = styled(DataGrid)({
|
||||
},
|
||||
});
|
||||
|
||||
const premiumColor = function (baseColor: string, accentColor: string, point: number) {
|
||||
const premiumColor = function (baseColor: string, accentColor: string, point: number): string {
|
||||
const baseRGB = hexToRgb(baseColor);
|
||||
const accentRGB = hexToRgb(accentColor);
|
||||
const redDiff = accentRGB[0] - baseRGB[0];
|
||||
const red = baseRGB[0] + redDiff * point;
|
||||
const red = Number(baseRGB[0]) + redDiff * point;
|
||||
const greenDiff = accentRGB[1] - baseRGB[1];
|
||||
const green = baseRGB[1] + greenDiff * point;
|
||||
const green = Number(baseRGB[1]) + greenDiff * point;
|
||||
const blueDiff = accentRGB[2] - baseRGB[2];
|
||||
const blue = baseRGB[2] + blueDiff * point;
|
||||
const blue = Number(baseRGB[2]) + blueDiff * point;
|
||||
return `rgb(${Math.round(red)}, ${Math.round(green)}, ${Math.round(blue)}, ${0.7 + point * 0.3})`;
|
||||
};
|
||||
|
||||
@ -86,7 +89,7 @@ interface BookTableProps {
|
||||
showControls?: boolean;
|
||||
showFooter?: boolean;
|
||||
showNoResults?: boolean;
|
||||
onOrderClicked?: (id: number) => void;
|
||||
onOrderClicked?: (id: number, shortAlias: string) => void;
|
||||
}
|
||||
|
||||
const BookTable = ({
|
||||
@ -103,11 +106,18 @@ const BookTable = ({
|
||||
showNoResults = true,
|
||||
onOrderClicked = () => null,
|
||||
}: BookTableProps): JSX.Element => {
|
||||
const { book, fetchBook, fav, setFav, baseUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { fav, setFav, settings, setOpen, hostUrl, origin } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, setFocusedCoordinator, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const orders = orderList ?? book.orders;
|
||||
const orders = orderList ?? federation.book;
|
||||
const loadingProgress = useMemo(() => {
|
||||
return (federation.exchange.onlineCoordinators / federation.exchange.totalCoordinators) * 100;
|
||||
}, [coordinatorUpdatedAt]);
|
||||
|
||||
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
||||
pageSize: 0,
|
||||
page: 0,
|
||||
@ -133,10 +143,10 @@ const BookTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationModel({
|
||||
pageSize: book.loading && orders.length == 0 ? 0 : defaultPageSize,
|
||||
pageSize: federation.loading && orders.length === 0 ? 0 : defaultPageSize,
|
||||
page: paginationModel.page,
|
||||
});
|
||||
}, [book.loading, orders, defaultPageSize]);
|
||||
}, [coordinatorUpdatedAt, orders, defaultPageSize]);
|
||||
|
||||
const localeText = useMemo(() => {
|
||||
return {
|
||||
@ -196,8 +206,17 @@ const BookTable = ({
|
||||
headerName: t('Robot'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(params.row.coordinatorShortAlias)
|
||||
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
|
||||
return (
|
||||
<ListItemButton style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}>
|
||||
<ListItemButton
|
||||
style={{ cursor: 'pointer', position: 'relative', left: '-1.3em' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
nickname={params.row.maker_nick}
|
||||
@ -207,7 +226,7 @@ const BookTable = ({
|
||||
orderType={params.row.type}
|
||||
statusColor={statusBadgeColor(params.row.maker_status)}
|
||||
tooltip={t(params.row.maker_status)}
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={url + basePath}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
@ -224,9 +243,17 @@ const BookTable = ({
|
||||
headerName: t('Robot'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(params.row.coordinatorShortAlias)
|
||||
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', left: '-1.64em' }}>
|
||||
<ListItemButton style={{ cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ position: 'relative', left: '-0.34em', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<RobotAvatar
|
||||
nickname={params.row.maker_nick}
|
||||
smooth={true}
|
||||
@ -235,11 +262,46 @@ const BookTable = ({
|
||||
orderType={params.row.type}
|
||||
statusColor={statusBadgeColor(params.row.maker_status)}
|
||||
tooltip={t(params.row.maker_status)}
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={url + basePath}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClickCoordinator = function (shortAlias: string): void {
|
||||
setFocusedCoordinator(shortAlias);
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: true };
|
||||
});
|
||||
};
|
||||
|
||||
const coordinatorObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'coordinatorShortAlias',
|
||||
headerName: t('Host'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<ListItemButton
|
||||
style={{ cursor: 'pointer', position: 'relative', left: '-1.54em' }}
|
||||
onClick={() => {
|
||||
onClickCoordinator(params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
nickname={params.row.coordinatorShortAlias}
|
||||
coordinator={true}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
baseUrl={hostUrl}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItemButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -250,10 +312,20 @@ const BookTable = ({
|
||||
field: 'type',
|
||||
headerName: t('Is'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) =>
|
||||
params.row.type
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{params.row.type === 1
|
||||
? t(fav.mode === 'fiat' ? 'Seller' : 'Swapping Out')
|
||||
: t(fav.mode === 'fiat' ? 'Buyer' : 'Swapping In'),
|
||||
: t(fav.mode === 'fiat' ? 'Buyer' : 'Swapping In')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -270,7 +342,12 @@ const BookTable = ({
|
||||
const maxAmount =
|
||||
fav.mode === 'swap' ? params.row.max_amount * 100000 : params.row.max_amount;
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{amountToString(amount, params.row.has_range, minAmount, maxAmount) +
|
||||
(fav.mode === 'swap' ? 'K Sats' : '')}
|
||||
</div>
|
||||
@ -285,7 +362,7 @@ const BookTable = ({
|
||||
headerName: t('Currency'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
const currencyCode = String(currencyDict[params.row.currency.toString()]);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -294,6 +371,9 @@ const BookTable = ({
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{currencyCode}
|
||||
<div style={{ width: '0.3em' }} />
|
||||
@ -311,7 +391,12 @@ const BookTable = ({
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
@ -337,6 +422,9 @@ const BookTable = ({
|
||||
left: '-4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
@ -356,9 +444,16 @@ const BookTable = ({
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
const currencyCode = String(currencyDict[params.row.currency.toString()]);
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>{`${pn(params.row.price)} ${currencyCode}/BTC`}</div>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{`${pn(params.row.price)} ${currencyCode}/BTC`}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -377,10 +472,11 @@ const BookTable = ({
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const currencyCode = currencyDict[params.row.currency.toString()];
|
||||
const currencyCode = String(currencyDict[params.row.currency.toString()]);
|
||||
let fontColor = `rgb(0,0,0)`;
|
||||
let premiumPoint = 0;
|
||||
if (params.row.type === 0) {
|
||||
var premiumPoint = params.row.premium / buyOutstandingPremium;
|
||||
premiumPoint = params.row.premium / buyOutstandingPremium;
|
||||
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
|
||||
fontColor = premiumColor(
|
||||
theme.palette.text.primary,
|
||||
@ -388,7 +484,7 @@ const BookTable = ({
|
||||
premiumPoint,
|
||||
);
|
||||
} else {
|
||||
var premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium;
|
||||
premiumPoint = (sellStandardPremium - params.row.premium) / sellStandardPremium;
|
||||
premiumPoint = premiumPoint < 0 ? 0 : premiumPoint > 1 ? 1 : premiumPoint;
|
||||
fontColor = premiumColor(
|
||||
theme.palette.text.primary,
|
||||
@ -401,11 +497,16 @@ const BookTable = ({
|
||||
<Tooltip
|
||||
placement='left'
|
||||
enterTouchDelay={0}
|
||||
title={pn(params.row.price) + ' ' + currencyCode + '/BTC'}
|
||||
title={`${pn(params.row.price)} ${currencyCode}/BTC`}
|
||||
>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<Typography variant='inherit' color={fontColor} sx={{ fontWeight }}>
|
||||
{parseFloat(parseFloat(params.row.premium).toFixed(4)) + '%'}
|
||||
{`${parseFloat(parseFloat(params.row.premium).toFixed(4))}%`}
|
||||
</Typography>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -425,7 +526,16 @@ const BookTable = ({
|
||||
renderCell: (params: any) => {
|
||||
const hours = Math.round(params.row.escrow_duration / 3600);
|
||||
const minutes = Math.round((params.row.escrow_duration - hours * 3600) / 60);
|
||||
return <div style={{ cursor: 'pointer' }}>{hours > 0 ? `${hours}h` : `${minutes}m`}</div>;
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{hours > 0 ? `${hours}h` : `${minutes}m`}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
@ -443,7 +553,12 @@ const BookTable = ({
|
||||
const hours = Math.round(timeToExpiry / (3600 * 1000));
|
||||
const minutes = Math.round((timeToExpiry - hours * (3600 * 1000)) / 60000);
|
||||
return (
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}>
|
||||
<Box
|
||||
sx={{ position: 'relative', display: 'inline-flex', left: '0.3em' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
value={percent}
|
||||
color={percent < 15 ? 'error' : percent < 30 ? 'warning' : 'success'}
|
||||
@ -481,7 +596,12 @@ const BookTable = ({
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{params.row.satoshis_now > 1000000
|
||||
? `${pn(Math.round(params.row.satoshis_now / 10000) / 100)} M`
|
||||
: `${pn(Math.round(params.row.satoshis_now / 1000))} K`}
|
||||
@ -498,9 +618,14 @@ const BookTable = ({
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{`#${params.row.id}`}
|
||||
{`#${String(params.row.id)}`}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
@ -515,7 +640,14 @@ const BookTable = ({
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return <div style={{ cursor: 'pointer' }}>{`${Number(params.row.bond_size)}%`}</div>;
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>{`${Number(params.row.bond_size)}%`}</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
@ -524,7 +656,7 @@ const BookTable = ({
|
||||
return {
|
||||
amount: {
|
||||
priority: 1,
|
||||
order: 4,
|
||||
order: 5,
|
||||
normal: {
|
||||
width: fav.mode === 'swap' ? 9.5 : 6.5,
|
||||
object: amountObj,
|
||||
@ -532,7 +664,7 @@ const BookTable = ({
|
||||
},
|
||||
currency: {
|
||||
priority: 2,
|
||||
order: 5,
|
||||
order: 6,
|
||||
normal: {
|
||||
width: fav.mode === 'swap' ? 0 : 5.9,
|
||||
object: currencyObj,
|
||||
@ -540,7 +672,7 @@ const BookTable = ({
|
||||
},
|
||||
premium: {
|
||||
priority: 3,
|
||||
order: 11,
|
||||
order: 12,
|
||||
normal: {
|
||||
width: 6,
|
||||
object: premiumObj,
|
||||
@ -548,7 +680,7 @@ const BookTable = ({
|
||||
},
|
||||
payment_method: {
|
||||
priority: 4,
|
||||
order: 6,
|
||||
order: 7,
|
||||
normal: {
|
||||
width: 12.85,
|
||||
object: paymentObj,
|
||||
@ -570,9 +702,17 @@ const BookTable = ({
|
||||
object: robotSmallObj,
|
||||
},
|
||||
},
|
||||
coordinatorShortAlias: {
|
||||
priority: 5,
|
||||
order: 3,
|
||||
normal: {
|
||||
width: 4.1,
|
||||
object: coordinatorObj,
|
||||
},
|
||||
},
|
||||
price: {
|
||||
priority: 6,
|
||||
order: 10,
|
||||
order: 11,
|
||||
normal: {
|
||||
width: 10,
|
||||
object: priceObj,
|
||||
@ -580,7 +720,7 @@ const BookTable = ({
|
||||
},
|
||||
expires_at: {
|
||||
priority: 7,
|
||||
order: 7,
|
||||
order: 8,
|
||||
normal: {
|
||||
width: 5,
|
||||
object: expiryObj,
|
||||
@ -588,7 +728,7 @@ const BookTable = ({
|
||||
},
|
||||
escrow_duration: {
|
||||
priority: 8,
|
||||
order: 8,
|
||||
order: 9,
|
||||
normal: {
|
||||
width: 4.8,
|
||||
object: timerObj,
|
||||
@ -596,7 +736,7 @@ const BookTable = ({
|
||||
},
|
||||
satoshis_now: {
|
||||
priority: 9,
|
||||
order: 9,
|
||||
order: 10,
|
||||
normal: {
|
||||
width: 6,
|
||||
object: satoshisObj,
|
||||
@ -612,7 +752,7 @@ const BookTable = ({
|
||||
},
|
||||
bond_size: {
|
||||
priority: 11,
|
||||
order: 10,
|
||||
order: 11,
|
||||
normal: {
|
||||
width: 4.2,
|
||||
object: bondObj,
|
||||
@ -620,7 +760,7 @@ const BookTable = ({
|
||||
},
|
||||
id: {
|
||||
priority: 12,
|
||||
order: 12,
|
||||
order: 13,
|
||||
normal: {
|
||||
width: 4.8,
|
||||
object: idObj,
|
||||
@ -629,7 +769,10 @@ const BookTable = ({
|
||||
};
|
||||
}, [fav.mode]);
|
||||
|
||||
const filteredColumns = function (maxWidth: number) {
|
||||
const filteredColumns = function (maxWidth: number): {
|
||||
columns: Array<GridColDef<GridValidRowModel>>;
|
||||
width: number;
|
||||
} {
|
||||
const useSmall = maxWidth < 70;
|
||||
const selectedColumns: object[] = [];
|
||||
const columnVisibilityModel: GridColumnVisibilityModel = {};
|
||||
@ -641,8 +784,10 @@ const BookTable = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const colWidth = useSmall && value.small ? value.small.width : value.normal.width;
|
||||
const colObject = useSmall && value.small ? value.small.object : value.normal.object;
|
||||
const colWidth = Number(
|
||||
useSmall && Boolean(value.small) ? value.small.width : value.normal.width,
|
||||
);
|
||||
const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object;
|
||||
|
||||
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
|
||||
width = width + colWidth;
|
||||
@ -659,19 +804,19 @@ const BookTable = ({
|
||||
return first[1] - second[1];
|
||||
});
|
||||
|
||||
const columns = selectedColumns.map(function (item) {
|
||||
const columns: Array<GridColDef<GridValidRowModel>> = selectedColumns.map(function (item) {
|
||||
return item[0];
|
||||
});
|
||||
|
||||
setColumnVisibilityModel(columnVisibilityModel);
|
||||
return [columns, width * 0.875 + 0.15];
|
||||
return { columns, width: width * 0.875 + 0.15 };
|
||||
};
|
||||
|
||||
const [columns, width] = useMemo(() => {
|
||||
const { columns, width } = useMemo(() => {
|
||||
return filteredColumns(fullscreen ? fullWidth : maxWidth);
|
||||
}, [maxWidth, fullscreen, fullWidth, fav.mode]);
|
||||
|
||||
const Footer = function () {
|
||||
const Footer = function (): JSX.Element {
|
||||
return (
|
||||
<Grid container alignItems='center' direction='row' justifyContent='space-between'>
|
||||
<Grid item>
|
||||
@ -686,11 +831,7 @@ const BookTable = ({
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
fetchBook();
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => federation.update()}>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
@ -712,7 +853,7 @@ const BookTable = ({
|
||||
Toolbar?: (props: any) => JSX.Element;
|
||||
}
|
||||
|
||||
const NoResultsOverlay = function () {
|
||||
const NoResultsOverlay = function (): JSX.Element {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@ -723,14 +864,14 @@ const BookTable = ({
|
||||
>
|
||||
<Grid item>
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
{fav.type == 0
|
||||
{fav.type === 0
|
||||
? t('No orders found to sell BTC for {{currencyCode}}', {
|
||||
currencyCode:
|
||||
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()],
|
||||
fav.currency === 0 ? t('ANY') : currencyDict[fav.currency.toString()],
|
||||
})
|
||||
: t('No orders found to buy BTC for {{currencyCode}}', {
|
||||
currencyCode:
|
||||
fav.currency == 0 ? t('ANY') : currencyDict[fav.currency.toString()],
|
||||
fav.currency === 0 ? t('ANY') : currencyDict[fav.currency.toString()],
|
||||
})}
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -786,7 +927,8 @@ const BookTable = ({
|
||||
rowHeight={3.714 * theme.typography.fontSize}
|
||||
headerHeight={3.25 * theme.typography.fontSize}
|
||||
rows={filteredOrders}
|
||||
loading={book.loading}
|
||||
getRowId={(params: PublicOrder) => `${String(params.coordinatorShortAlias)}/${params.id}`}
|
||||
loading={federation.loading}
|
||||
columns={columns}
|
||||
columnVisibilityModel={columnVisibilityModel}
|
||||
onColumnVisibilityModelChange={(newColumnVisibilityModel) => {
|
||||
@ -802,15 +944,16 @@ const BookTable = ({
|
||||
paymentMethod: paymentMethods,
|
||||
setPaymentMethods,
|
||||
},
|
||||
loadingOverlay: {
|
||||
variant: 'determinate',
|
||||
value: loadingProgress,
|
||||
},
|
||||
}}
|
||||
paginationModel={paginationModel}
|
||||
pageSizeOptions={width < 22 ? [] : [0, defaultPageSize, defaultPageSize * 2, 50, 100]}
|
||||
onPaginationModelChange={(newPaginationModel) => {
|
||||
setPaginationModel(newPaginationModel);
|
||||
}}
|
||||
onRowClick={(params: any) => {
|
||||
onOrderClicked(params.row.id);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
@ -823,7 +966,7 @@ const BookTable = ({
|
||||
rowHeight={3.714 * theme.typography.fontSize}
|
||||
headerHeight={3.25 * theme.typography.fontSize}
|
||||
rows={filteredOrders}
|
||||
loading={book.loading}
|
||||
loading={federation.loading}
|
||||
columns={columns}
|
||||
hideFooter={!showFooter}
|
||||
components={gridComponents}
|
||||
@ -845,9 +988,6 @@ const BookTable = ({
|
||||
onPaginationModelChange={(newPaginationModel) => {
|
||||
setPaginationModel(newPaginationModel);
|
||||
}}
|
||||
onRowClick={(params: any) => {
|
||||
onOrderClicked(params.row.id);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Dialog>
|
||||
|
@ -24,15 +24,16 @@ import { type PublicOrder, type Order } from '../../../models';
|
||||
import { matchMedian } from '../../../utils';
|
||||
import currencyDict from '../../../../static/assets/currencies.json';
|
||||
import getNivoScheme from '../NivoScheme';
|
||||
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
|
||||
import OrderTooltip from '../helpers/OrderTooltip';
|
||||
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../../contexts/FederationContext';
|
||||
|
||||
interface DepthChartProps {
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
fillContainer?: boolean;
|
||||
elevation?: number;
|
||||
onOrderClicked?: (id: number) => void;
|
||||
onOrderClicked?: (id: number, shortAlias: string) => void;
|
||||
}
|
||||
|
||||
const DepthChart: React.FC<DepthChartProps> = ({
|
||||
@ -42,10 +43,12 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
elevation = 6,
|
||||
onOrderClicked = () => null,
|
||||
}) => {
|
||||
const { book, fav, info, limits, baseUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { fav } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, coordinatorUpdatedAt, federationUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [enrichedOrders, setEnrichedOrders] = useState<Order[]>([]);
|
||||
const [enrichedOrders, setEnrichedOrders] = useState<PublicOrder[]>([]);
|
||||
const [series, setSeries] = useState<Serie[]>([]);
|
||||
const [rangeSteps, setRangeSteps] = useState<number>(8);
|
||||
const [xRange, setXRange] = useState<number>(8);
|
||||
@ -61,18 +64,21 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
}, [fav.currency]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(limits.list).length > 0) {
|
||||
const enriched = book.orders.map((order) => {
|
||||
if (federation.book.length > 0) {
|
||||
const enriched = federation.book.map((order) => {
|
||||
// We need to transform all currencies to the same base (ex. USD), we don't have the exchange rate
|
||||
// for EUR -> USD, but we know the rate of both to BTC, so we get advantage of it and apply a
|
||||
// simple rule of three
|
||||
if (order.coordinatorShortAlias) {
|
||||
const limits = federation.getCoordinator(order.coordinatorShortAlias).limits;
|
||||
order.base_amount =
|
||||
(order.price * limits.list[currencyCode].price) / limits.list[order.currency].price;
|
||||
(order.price * limits[currencyCode].price) / limits[order.currency].price;
|
||||
}
|
||||
return order;
|
||||
});
|
||||
setEnrichedOrders(enriched);
|
||||
}
|
||||
}, [limits.list, book.orders, currencyCode]);
|
||||
}, [coordinatorUpdatedAt, currencyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enrichedOrders.length > 0) {
|
||||
@ -82,10 +88,10 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (xType === 'base_amount') {
|
||||
const prices: number[] = enrichedOrders.map((order) => order?.base_amount || 0);
|
||||
const prices: number[] = enrichedOrders.map((order) => order?.base_amount ?? 0);
|
||||
|
||||
const medianValue = ~~matchMedian(prices);
|
||||
const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] || 1500;
|
||||
const maxValue = prices.sort((a, b) => b - a).slice(0, 1)[0] ?? 1500;
|
||||
const maxRange = maxValue - medianValue;
|
||||
const rangeSteps = maxRange / 10;
|
||||
|
||||
@ -93,35 +99,35 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
setXRange(maxRange);
|
||||
setRangeSteps(rangeSteps);
|
||||
} else {
|
||||
if (info.last_day_nonkyc_btc_premium === undefined) {
|
||||
const premiums: number[] = enrichedOrders.map((order) => order?.premium || 0);
|
||||
if (federation.exchange.info?.last_day_nonkyc_btc_premium === undefined) {
|
||||
const premiums: number[] = enrichedOrders.map((order) => order?.premium ?? 0);
|
||||
setCenter(~~matchMedian(premiums));
|
||||
} else {
|
||||
setCenter(info.last_day_nonkyc_btc_premium);
|
||||
setCenter(federation.exchange.info?.last_day_nonkyc_btc_premium);
|
||||
}
|
||||
setXRange(8);
|
||||
setRangeSteps(0.5);
|
||||
}
|
||||
}, [enrichedOrders, xType, info.last_day_nonkyc_btc_premium, currencyCode]);
|
||||
}, [enrichedOrders, xType, federationUpdatedAt, currencyCode]);
|
||||
|
||||
const generateSeries: () => void = () => {
|
||||
const sortedOrders: PublicOrder[] =
|
||||
xType === 'base_amount'
|
||||
? enrichedOrders.sort(
|
||||
(order1, order2) => (order1?.base_amount || 0) - (order2?.base_amount || 0),
|
||||
(order1, order2) => (order1?.base_amount ?? 0) - (order2?.base_amount ?? 0),
|
||||
)
|
||||
: enrichedOrders.sort((order1, order2) => order1.premium - order2.premium);
|
||||
|
||||
const sortedBuyOrders: PublicOrder[] = sortedOrders
|
||||
.filter((order) => order.type == 0)
|
||||
.filter((order) => order.type === 0)
|
||||
.reverse();
|
||||
const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type == 1);
|
||||
const sortedSellOrders: PublicOrder[] = sortedOrders.filter((order) => order.type === 1);
|
||||
|
||||
const buySerie: Datum[] = generateSerie(sortedBuyOrders);
|
||||
const sellSerie: Datum[] = generateSerie(sortedSellOrders);
|
||||
|
||||
const maxX: number = (center || 0) + xRange;
|
||||
const minX: number = (center || 0) - xRange;
|
||||
const maxX: number = (center ?? 0) + xRange;
|
||||
const minX: number = (center ?? 0) - xRange;
|
||||
|
||||
setSeries([
|
||||
{
|
||||
@ -136,7 +142,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
};
|
||||
|
||||
const generateSerie = (orders: PublicOrder[]): Datum[] => {
|
||||
if (center == undefined) {
|
||||
if (center === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -144,7 +150,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
let serie: Datum[] = [];
|
||||
orders.forEach((order) => {
|
||||
const lastSumOrders = sumOrders;
|
||||
sumOrders += (order.satoshis_now || 0) / 100000000;
|
||||
sumOrders += (order.satoshis_now ?? 0) / 100000000;
|
||||
const datum: Datum[] = [
|
||||
{
|
||||
// Vertical Line
|
||||
@ -169,7 +175,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
};
|
||||
|
||||
const closeSerie = (serie: Datum[], limitBottom: number, limitTop: number): Datum[] => {
|
||||
if (serie.length == 0) {
|
||||
if (serie.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -197,11 +203,11 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
d={props.lineGenerator([
|
||||
{
|
||||
y: 0,
|
||||
x: props.xScale(center || 0),
|
||||
x: props.xScale(center ?? 0),
|
||||
},
|
||||
{
|
||||
y: props.innerHeight,
|
||||
x: props.xScale(center || 0),
|
||||
x: props.xScale(center ?? 0),
|
||||
},
|
||||
])}
|
||||
fill='none'
|
||||
@ -225,7 +231,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
};
|
||||
const formatAxisY = (value: number): string => `${value}BTC`;
|
||||
const handleOnClick: PointMouseHandler = (point: Point) => {
|
||||
onOrderClicked(point.data?.order?.id);
|
||||
onOrderClicked(point.data?.order?.id, point.data?.order?.coordinatorShortAlias);
|
||||
};
|
||||
|
||||
const em = theme.typography.fontSize;
|
||||
@ -239,7 +245,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
}
|
||||
>
|
||||
<Paper variant='outlined' style={{ width: '100%', height: '100%' }}>
|
||||
{center == undefined || enrichedOrders.length < 1 ? (
|
||||
{center === undefined || enrichedOrders.length < 1 ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@ -299,7 +305,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
<Grid item>
|
||||
<Box justifyContent='center'>
|
||||
{xType === 'base_amount'
|
||||
? `${center} ${currencyDict[currencyCode]}`
|
||||
? `${center} ${String(currencyDict[currencyCode])}`
|
||||
: `${center}%`}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
@ -12,9 +12,10 @@ import {
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import Map from '../../Map';
|
||||
import { AppContext, UseAppStoreType } from '../../../contexts/AppContext';
|
||||
import { AppContext, type UseAppStoreType } from '../../../contexts/AppContext';
|
||||
import { PhotoSizeSelectActual } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FederationContext, UseFederationStoreType } from '../../../contexts/FederationContext';
|
||||
|
||||
interface MapChartProps {
|
||||
maxWidth: number;
|
||||
@ -32,7 +33,7 @@ const MapChart: React.FC<MapChartProps> = ({
|
||||
onOrderClicked = () => {},
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { book } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const [useTiles, setUseTiles] = useState<boolean>(false);
|
||||
const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false);
|
||||
const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false);
|
||||
@ -81,7 +82,7 @@ const MapChart: React.FC<MapChartProps> = ({
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Paper variant='outlined' style={{ width: '100%', height: '100%', justifyContent: 'center' }}>
|
||||
{false ? (
|
||||
{federation.book.length < 1 ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@ -127,7 +128,7 @@ const MapChart: React.FC<MapChartProps> = ({
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<div style={{ height: `${height - 3.1}em` }}>
|
||||
<Map useTiles={useTiles} orders={book.orders} onOrderClicked={onOrderClicked} />
|
||||
<Map useTiles={useTiles} orders={federation.book} onOrderClicked={onOrderClicked} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -6,17 +6,24 @@ import { amountToString, statusBadgeColor } from '../../../../utils';
|
||||
import currencyDict from '../../../../../static/assets/currencies.json';
|
||||
import { PaymentStringAsIcons } from '../../../PaymentMethods';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AppContext, UseAppStoreType } from '../../../../contexts/AppContext';
|
||||
import { AppContext, type UseAppStoreType } from '../../../../contexts/AppContext';
|
||||
import { FederationContext, UseFederationStoreType } from '../../../../contexts/FederationContext';
|
||||
|
||||
interface OrderTooltipProps {
|
||||
order: PublicOrder;
|
||||
}
|
||||
|
||||
const OrderTooltip: React.FC<OrderTooltipProps> = ({ order }) => {
|
||||
const { baseUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { settings, origin } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return order ? (
|
||||
const coordinatorAlias = order?.coordinatorShortAlias;
|
||||
const network = settings.network;
|
||||
const coordinator = federation.getCoordinator(coordinatorAlias);
|
||||
const baseUrl = coordinator?.[network]?.[origin] ?? '';
|
||||
|
||||
return order?.id && baseUrl !== '' ? (
|
||||
<Paper elevation={12} style={{ padding: 10, width: 250 }}>
|
||||
<Grid container justifyContent='space-between'>
|
||||
<Grid item xs={3}>
|
||||
|
@ -22,7 +22,7 @@ interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
const AboutDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -263,4 +263,4 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
export default InfoDialog;
|
||||
export default AboutDialog;
|
@ -23,7 +23,7 @@ import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import ForumIcon from '@mui/icons-material/Forum';
|
||||
import { ExportIcon, NewTabIcon } from '../Icons';
|
||||
|
||||
function CredentialTextfield(props) {
|
||||
function CredentialTextfield(props): JSX.Element {
|
||||
return (
|
||||
<Grid item align='center' xs={12}>
|
||||
<Tooltip placement='top' enterTouchDelay={200} enterDelay={200} title={props.tooltipTitle}>
|
||||
@ -58,9 +58,9 @@ interface Props {
|
||||
onClose: () => void;
|
||||
orderId: number;
|
||||
messages: array;
|
||||
own_pub_key: string;
|
||||
own_enc_priv_key: string;
|
||||
peer_pub_key: string;
|
||||
ownPubKey: string;
|
||||
ownEncPrivKey: string;
|
||||
peerPubKey: string;
|
||||
passphrase: string;
|
||||
onClickBack: () => void;
|
||||
}
|
||||
@ -70,9 +70,9 @@ const AuditPGPDialog = ({
|
||||
onClose,
|
||||
orderId,
|
||||
messages,
|
||||
own_pub_key,
|
||||
own_enc_priv_key,
|
||||
peer_pub_key,
|
||||
ownPubKey,
|
||||
ownEncPrivKey,
|
||||
peerPubKey,
|
||||
passphrase,
|
||||
onClickBack,
|
||||
}: Props): JSX.Element => {
|
||||
@ -104,7 +104,7 @@ const AuditPGPDialog = ({
|
||||
'Your PGP public key. Your peer uses it to encrypt messages only you can read.',
|
||||
)}
|
||||
label={t('Your public key')}
|
||||
value={own_pub_key}
|
||||
value={ownPubKey}
|
||||
copiedTitle={t('Copied!')}
|
||||
/>
|
||||
|
||||
@ -113,7 +113,7 @@ const AuditPGPDialog = ({
|
||||
'Your peer PGP public key. You use it to encrypt messages only he can read and to verify your peer signed the incoming messages.',
|
||||
)}
|
||||
label={t('Peer public key')}
|
||||
value={peer_pub_key}
|
||||
value={peerPubKey}
|
||||
copiedTitle={t('Copied!')}
|
||||
/>
|
||||
|
||||
@ -122,7 +122,7 @@ const AuditPGPDialog = ({
|
||||
'Your encrypted private key. You use it to decrypt the messages that your peer encrypted for you. You also use it to sign the messages you send.',
|
||||
)}
|
||||
label={t('Your encrypted private key')}
|
||||
value={own_enc_priv_key}
|
||||
value={ownEncPrivKey}
|
||||
copiedTitle={t('Copied!')}
|
||||
/>
|
||||
|
||||
@ -149,10 +149,10 @@ const AuditPGPDialog = ({
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
saveAsJson('keys_' + orderId + '.json', {
|
||||
own_public_key: own_pub_key,
|
||||
peer_public_key: peer_pub_key,
|
||||
encrypted_private_key: own_enc_priv_key,
|
||||
saveAsJson(`keys_${orderId}.json`, {
|
||||
own_public_key: ownPubKey,
|
||||
peer_public_key: peerPubKey,
|
||||
encrypted_private_key: ownEncPrivKey,
|
||||
passphrase,
|
||||
});
|
||||
}}
|
||||
@ -181,7 +181,7 @@ const AuditPGPDialog = ({
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
saveAsJson('messages_' + orderId + '.json', messages);
|
||||
saveAsJson(`messages_${orderId}.json`, messages);
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 28, height: 20 }}>
|
||||
|
80
frontend/src/components/Dialogs/Client.tsx
Normal file
80
frontend/src/components/Dialogs/Client.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
|
||||
import { RoboSatsNoTextIcon } from '../Icons';
|
||||
import { AppContext, type AppContextProps } from '../../contexts/AppContext';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ClientDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { clientVersion } = useContext<AppContextProps>(AppContext);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Client info')}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<RoboSatsNoTextIcon
|
||||
sx={{ width: '1.4em', height: '1.4em', right: '0.2em', position: 'relative' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={clientVersion.long} secondary={t('RoboSats client version')} />
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PublicIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'left',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<span>{`${t('Made with')} `}</span>
|
||||
<FavoriteIcon sx={{ color: '#ff0000', height: '22px', width: '22px' }} />
|
||||
<span>{` ${t('and')} `}</span>
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '23px', width: '23px' }} />
|
||||
</div>
|
||||
}
|
||||
secondary={t('... somewhere on Earth!')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientDialog;
|
@ -156,7 +156,7 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText secondary={t('We are abandoning Telegram! Our old TG groups')}>
|
||||
<Tooltip title={t('Join RoboSats Spanish speaking community!') || ''}>
|
||||
<Tooltip title={t('Join RoboSats Spanish speaking community!')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
@ -167,7 +167,7 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('Join RoboSats English speaking community!') || ''}>
|
||||
<Tooltip title={t('Join RoboSats English speaking community!')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
|
754
frontend/src/components/Dialogs/Coordinator.tsx
Normal file
754
frontend/src/components/Dialogs/Coordinator.tsx
Normal file
@ -0,0 +1,754 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
Grid,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Link,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Inventory,
|
||||
Sell,
|
||||
SmartToy,
|
||||
Percent,
|
||||
PriceChange,
|
||||
Book,
|
||||
Reddit,
|
||||
Key,
|
||||
Bolt,
|
||||
Description,
|
||||
Dns,
|
||||
Email,
|
||||
Equalizer,
|
||||
ExpandMore,
|
||||
GitHub,
|
||||
Language,
|
||||
Send,
|
||||
Tag,
|
||||
Twitter,
|
||||
} from '@mui/icons-material';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
|
||||
import { pn } from '../../utils';
|
||||
import { type Contact, type Coordinator } from '../../models';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import {
|
||||
AmbossIcon,
|
||||
BitcoinSignIcon,
|
||||
RoboSatsNoTextIcon,
|
||||
BadgeFounder,
|
||||
BadgeDevFund,
|
||||
BadgePrivacy,
|
||||
BadgeLoved,
|
||||
BadgeLimits,
|
||||
NostrIcon,
|
||||
} from '../Icons';
|
||||
import { AppContext } from '../../contexts/AppContext';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { type Badges } from '../../models/Coordinator.model';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
coordinator: Coordinator | null;
|
||||
network: 'mainnet' | 'testnet' | undefined;
|
||||
}
|
||||
|
||||
const ContactButtons = ({
|
||||
nostr,
|
||||
pgp,
|
||||
email,
|
||||
telegram,
|
||||
twitter,
|
||||
matrix,
|
||||
website,
|
||||
reddit,
|
||||
}: Contact): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [showMatrix, setShowMatrix] = useState<boolean>(false);
|
||||
const [showNostr, setShowNostr] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Grid container direction='row' alignItems='center' justifyContent='center'>
|
||||
{nostr !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<Typography variant='body2'>
|
||||
{t('...Opening on Nostr gateway. Pubkey copied!')}
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
<i>{nostr}</i>
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
open={showNostr}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShowNostr(true);
|
||||
setTimeout(() => window.open(`https://snort.social/p/${nostr}`, '_blank'), 1500);
|
||||
setTimeout(() => {
|
||||
setShowNostr(false);
|
||||
}, 10000);
|
||||
systemClient.copyToClipboard(nostr);
|
||||
}}
|
||||
>
|
||||
<NostrIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{pgp !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('See PGP Key')}>
|
||||
<IconButton component='a' target='_blank' href={`https://${pgp}`} rel='noreferrer'>
|
||||
<Key />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{email !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Send Email')}>
|
||||
<IconButton component='a' href={`mailto: ${email}`}>
|
||||
<Email />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{telegram !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Telegram')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href={`https://t.me/${telegram}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<Send />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{twitter !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Twitter')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href={`https://twitter.com/${twitter}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<Twitter />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{reddit !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Reddit')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href={`https://reddit.com/${reddit}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<Reddit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{website !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Website')}>
|
||||
<IconButton component='a' target='_blank' href={website} rel='noreferrer'>
|
||||
<Language />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{matrix !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography variant='body2'>
|
||||
{t('Matrix channel copied! {{matrix}}', { matrix })}
|
||||
</Typography>
|
||||
}
|
||||
open={showMatrix}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setShowMatrix(true);
|
||||
setTimeout(() => {
|
||||
setShowMatrix(false);
|
||||
}, 10000);
|
||||
systemClient.copyToClipboard(matrix);
|
||||
}}
|
||||
>
|
||||
<Tag />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgesProps {
|
||||
badges: Badges | undefined;
|
||||
}
|
||||
|
||||
const BadgesHall = ({ badges }: BadgesProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const sxProps = {
|
||||
width: '3em',
|
||||
height: '3em',
|
||||
filter: 'drop-shadow(3px 3px 3px RGB(0,0,0,0.3))',
|
||||
};
|
||||
const tooltipProps = { enterTouchDelay: 0, enterNextDelay: 2000 };
|
||||
return (
|
||||
<Grid container direction='row' alignItems='center' justifyContent='center' spacing={1}>
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
title={
|
||||
<Typography align='center' variant='body2'>
|
||||
{badges?.isFounder === true
|
||||
? t('Founder: coordinating trades since the testnet federation.')
|
||||
: t('Not a federation founder')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid item sx={{ filter: badges?.isFounder !== true ? 'grayscale(100%)' : undefined }}>
|
||||
<BadgeFounder sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
title={
|
||||
<Typography align='center' variant='body2'>
|
||||
{t('Development fund supporter: donates {{percent}}% to make RoboSats better.', {
|
||||
percent: badges?.donatesToDevFund,
|
||||
})}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
sx={{ filter: Number(badges?.donatesToDevFund) >= 20 ? undefined : 'grayscale(100%)' }}
|
||||
>
|
||||
<BadgeDevFund sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
title={
|
||||
<Typography align='center' variant='body2'>
|
||||
{badges?.hasGoodOpSec === true
|
||||
? t(
|
||||
'Good OpSec: the coordinator follows best practices to protect his and your privacy.',
|
||||
)
|
||||
: t('The privacy practices of this coordinator could improve')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid item sx={{ filter: badges?.hasGoodOpSec === true ? undefined : 'grayscale(100%)' }}>
|
||||
<BadgePrivacy sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
title={
|
||||
<Typography align='center' variant='body2'>
|
||||
{badges?.robotsLove === true
|
||||
? t('Loved by robots: receives positive comments by robots over the internet.')
|
||||
: t(
|
||||
'The coordinator does not seem to receive exceptional love from robots over the internet',
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid item sx={{ filter: badges?.robotsLove === true ? undefined : 'grayscale(100%)' }}>
|
||||
<BadgeLoved sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
{...tooltipProps}
|
||||
title={
|
||||
<Typography align='center' variant='body2'>
|
||||
{badges?.hasLargeLimits === true
|
||||
? t('Large limits: the coordinator has large trade limits.')
|
||||
: t('Does not have large trade limits.')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid item sx={{ filter: badges?.hasLargeLimits === true ? undefined : 'grayscale(100%)' }}>
|
||||
<BadgeLimits sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const CoordinatorDialog = ({ open = false, onClose, coordinator, network }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { clientVersion, page, hostUrl } = useContext(AppContext);
|
||||
|
||||
const [expanded, setExpanded] = useState<'summary' | 'stats' | 'policies' | undefined>(undefined);
|
||||
|
||||
const listItemProps = { sx: { maxHeight: '3em', width: '100%' } };
|
||||
const coordinatorVersion = `v${coordinator?.info?.version?.major ?? '?'}.${
|
||||
coordinator?.info?.version?.minor ?? '?'
|
||||
}.${coordinator?.info?.version?.patch ?? '?'}`;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogContent>
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
{String(coordinator?.longAlias)}
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container direction='column' alignItems='center' padding={0}>
|
||||
<Grid item>
|
||||
<RobotAvatar
|
||||
nickname={coordinator?.shortAlias}
|
||||
coordinator={true}
|
||||
style={{ width: '7.5em', height: '7.5em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={false}
|
||||
baseUrl={hostUrl}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography align='center' variant='body2'>
|
||||
<i>{String(coordinator?.motto)}</i>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ContactButtons {...coordinator?.contact} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
||||
{['create'].includes(page) && (
|
||||
<>
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Percent />
|
||||
</ListItemIcon>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<ListItemText secondary={t('Maker fee')}>
|
||||
{(coordinator?.info?.maker_fee ?? 0 * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<ListItemText secondary={t('Taker fee')}>
|
||||
{(coordinator?.info?.taker_fee ?? 0 * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${String(coordinator?.info?.current_swap_fee_rate.toPrecision(3))}%`}
|
||||
secondary={t('Current onchain payout fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
<ListItem>
|
||||
<BadgesHall badges={coordinator?.badges} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Description />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.description}
|
||||
primaryTypographyProps={{ sx: { maxWidth: '20em' } }}
|
||||
secondary={t('Coordinator description')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{coordinator?.mainnetNodesPubkeys?.[0] !== undefined && network === 'mainnet' ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AmbossIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Mainnet LN Node')}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://amboss.space/node/${coordinator?.mainnetNodesPubkeys[0]}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${coordinator?.mainnetNodesPubkeys?.[0].slice(0, 12)}... (AMBOSS)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{coordinator?.testnetNodesPubkeys?.[0] !== undefined && network === 'testnet' ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Dns />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Testnet LN Node')}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://1ml.com/testnet/node/${coordinator?.testnetNodesPubkeys[0]}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${coordinator?.testnetNodesPubkeys[0].slice(0, 12)}... (1ML)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</List>
|
||||
|
||||
{coordinator?.loadingInfo === true ? (
|
||||
<Box style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : coordinator?.info !== undefined ? (
|
||||
<Box>
|
||||
{coordinator?.policies !== undefined && (
|
||||
<Accordion
|
||||
expanded={expanded === 'policies'}
|
||||
onChange={() => {
|
||||
setExpanded(expanded === 'policies' ? undefined : 'policies');
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Typography>{t('Policies')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ padding: 0 }}>
|
||||
<List dense>
|
||||
{Object.keys(coordinator?.policies).map((key, index) => (
|
||||
<ListItem key={index} sx={{ maxWidth: '24em' }}>
|
||||
<ListItemIcon>{index + 1}</ListItemIcon>
|
||||
<ListItemText primary={key} secondary={coordinator?.policies[key]} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
<Accordion
|
||||
expanded={expanded === 'summary'}
|
||||
onChange={() => {
|
||||
setExpanded(expanded === 'summary' ? undefined : 'summary');
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Typography>{t('Summary')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ padding: 0 }}>
|
||||
<List dense>
|
||||
<Divider />
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Percent />
|
||||
</ListItemIcon>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<ListItemText secondary={t('Maker fee')}>
|
||||
{(coordinator?.info?.maker_fee * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<ListItemText secondary={t('Taker fee')}>
|
||||
{(coordinator?.info?.taker_fee * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${coordinator?.info?.current_swap_fee_rate.toPrecision(3)}%`}
|
||||
secondary={t('Current onchain payout fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Inventory />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.info?.num_public_buy_orders}
|
||||
secondary={t('Public buy orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Sell />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.info?.num_public_sell_orders}
|
||||
secondary={t('Public sell orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Book />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${pn(coordinator?.info?.book_liquidity)} Sats`}
|
||||
secondary={t('Book liquidity')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<SmartToy />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.info?.active_robots_today}
|
||||
secondary={t('Today active robots')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${coordinator?.info?.last_day_nonkyc_btc_premium}%`}
|
||||
secondary={t('24h non-KYC bitcoin premium')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion
|
||||
expanded={expanded === 'stats'}
|
||||
onChange={() => {
|
||||
setExpanded(expanded === 'stats' ? undefined : 'stats');
|
||||
}}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Typography>{t('Stats for Nerds')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List dense>
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<RoboSatsNoTextIcon
|
||||
sx={{
|
||||
width: '1.4em',
|
||||
height: '1.4em',
|
||||
right: '0.2em',
|
||||
position: 'relative',
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${t('Coordinator')} ${coordinatorVersion} - ${t('Client')} ${String(
|
||||
clientVersion.short,
|
||||
)}`}
|
||||
secondary={t('RoboSats version')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{coordinator?.info?.lnd_version !== undefined && (
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Bolt />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={coordinator?.info?.lnd_version}
|
||||
secondary={t('LND version')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{Boolean(coordinator?.info?.cln_version) && (
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Bolt />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={coordinator?.info?.cln_version}
|
||||
secondary={t('CLN version')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{coordinator?.info?.network === 'testnet' ? (
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Dns />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={`${t('LN Node')}: ${coordinator?.info?.node_alias}`}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://1ml.com/testnet/node/${coordinator?.info?.node_id}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${coordinator?.info?.node_id.slice(0, 12)}... (1ML)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<AmbossIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={coordinator?.info?.node_alias}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://amboss.space/node/${coordinator?.info?.node_id}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${coordinator?.info?.node_id.slice(0, 12)}... (AMBOSS)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<GitHub />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Coordinator commit hash')}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://github.com/Reckless-Satoshi/robosats/tree/${coordinator?.info?.robosats_running_commit_hash}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${coordinator?.info?.robosats_running_commit_hash.slice(0, 12)}...`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Equalizer />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('24h contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(parseFloat(coordinator?.info?.last_day_volume).toFixed(8))}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Equalizer />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Lifetime contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(parseFloat(coordinator?.info?.lifetime_volume).toFixed(8))}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography align='center' variant='h6' color='error'>
|
||||
{t('Coordinator offline')}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoordinatorDialog;
|
@ -1,174 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
Grid,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import InventoryIcon from '@mui/icons-material/Inventory';
|
||||
import SellIcon from '@mui/icons-material/Sell';
|
||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import PercentIcon from '@mui/icons-material/Percent';
|
||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
import BookIcon from '@mui/icons-material/Book';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
|
||||
import { pn } from '../../utils';
|
||||
import { type Info } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
info: Info;
|
||||
}
|
||||
|
||||
const CoordinatorSummaryDialog = ({ open = false, onClose, info }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
if (info.current_swap_fee_rate === null || info.current_swap_fee_rate === undefined) {
|
||||
info.current_swap_fee_rate = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<div style={info.loading ? {} : { display: 'none' }}>
|
||||
<LinearProgress />
|
||||
</div>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Coordinator Summary')}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<InventoryIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={info.num_public_buy_orders}
|
||||
secondary={t('Public buy orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SellIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={info.num_public_sell_orders}
|
||||
secondary={t('Public sell orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<BookIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={`${pn(info.book_liquidity)} Sats`}
|
||||
secondary={t('Book liquidity')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SmartToyIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={info.active_robots_today}
|
||||
secondary={t('Today active robots')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PriceChangeIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={`${info.last_day_nonkyc_btc_premium}%`}
|
||||
secondary={t('Last 24h mean premium')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PercentIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
secondary={t('Maker fee')}
|
||||
>
|
||||
{(info.maker_fee * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6}>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
secondary={t('Taker fee')}
|
||||
>
|
||||
{(info.taker_fee * 100).toFixed(3)}%
|
||||
</ListItemText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primaryTypographyProps={{ fontSize: '14px' }}
|
||||
secondaryTypographyProps={{ fontSize: '12px' }}
|
||||
primary={`${info.current_swap_fee_rate.toPrecision(3)}%`}
|
||||
secondary={t('Current onchain payout fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoordinatorSummaryDialog;
|
@ -27,12 +27,12 @@ const EnableTelegramDialog = ({ open, onClose, tgBotName, tgToken }: Props): JSX
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleClickOpenBrowser = () => {
|
||||
const handleClickOpenBrowser = (): void => {
|
||||
window.open(`https://t.me/${tgBotName}?start=${tgToken}`, '_blank').focus();
|
||||
setOpenEnableTelegram(false);
|
||||
};
|
||||
|
||||
const handleOpenTG = () => {
|
||||
const handleOpenTG = (): void => {
|
||||
window.open(`tg://resolve?domain=${tgBotName}&start=${tgToken}`);
|
||||
};
|
||||
|
||||
|
190
frontend/src/components/Dialogs/Exchange.tsx
Normal file
190
frontend/src/components/Dialogs/Exchange.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Inventory,
|
||||
Sell,
|
||||
SmartToy,
|
||||
PriceChange,
|
||||
Book,
|
||||
Groups3,
|
||||
Equalizer,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { pn } from '../../utils';
|
||||
import { BitcoinSignIcon } from '../Icons';
|
||||
import { FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExchangeDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { federation } = useContext(FederationContext);
|
||||
|
||||
const loadingProgress = useMemo(() => {
|
||||
return (federation.exchange.onlineCoordinators / federation.exchange.totalCoordinators) * 100;
|
||||
}, [federation.exchange.onlineCoordinators, federation.exchange.totalCoordinators]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<div style={loadingProgress < 100 ? {} : { display: 'none' }}>
|
||||
<LinearProgress variant='determinate' value={loadingProgress} />
|
||||
</div>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Exchange Summary')}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Groups3 />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={federation.exchange.onlineCoordinators}
|
||||
secondary={t('Online RoboSats coordinators')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Groups3 />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={federation.exchange.totalCoordinators}
|
||||
secondary={t('Enabled RoboSats coordinators')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Inventory />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={federation.exchange.info.num_public_buy_orders}
|
||||
secondary={t('Public buy orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Sell />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={federation.exchange.info.num_public_sell_orders}
|
||||
secondary={t('Public sell orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Book />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${pn(federation.exchange.info.book_liquidity)} Sats`}
|
||||
secondary={t('Book liquidity')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SmartToy />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={federation.exchange.info.active_robots_today}
|
||||
secondary={t('Today active robots')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${String(
|
||||
federation.exchange.info.last_day_nonkyc_btc_premium.toPrecision(3),
|
||||
)}%`}
|
||||
secondary={t('24h non-KYC bitcoin premium')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Equalizer />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('24h contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(federation.exchange.info.last_day_volume)}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color='text.secondary' />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Equalizer />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Lifetime contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(federation.exchange.info.lifetime_volume)}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color='text.secondary' />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExchangeDialog;
|
@ -31,7 +31,7 @@ const F2fMapDialog = ({
|
||||
onClose = () => {},
|
||||
latitude,
|
||||
longitude,
|
||||
interactive,
|
||||
interactive = false,
|
||||
zoom,
|
||||
message = '',
|
||||
}: Props): JSX.Element => {
|
||||
@ -41,8 +41,8 @@ const F2fMapDialog = ({
|
||||
const [acceptedTilesWarning, setAcceptedTilesWarning] = useState<boolean>(false);
|
||||
const [openWarningDialog, setOpenWarningDialog] = useState<boolean>(false);
|
||||
|
||||
const onSave = () => {
|
||||
if (position && position[0] && position[1]) {
|
||||
const onSave: () => void = () => {
|
||||
if (position?.[0] && position?.[1]) {
|
||||
onClose([position[0] + Math.random() * 0.1 - 0.05, position[1] + Math.random() * 0.1 - 0.05]);
|
||||
}
|
||||
};
|
||||
@ -59,7 +59,9 @@ const F2fMapDialog = ({
|
||||
<Dialog
|
||||
open={open}
|
||||
fullWidth
|
||||
onClose={() => onClose()}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
aria-labelledby='worldmap-dialog-title'
|
||||
aria-describedby='worldmap-description'
|
||||
maxWidth={false}
|
||||
@ -154,15 +156,22 @@ const F2fMapDialog = ({
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{interactive ? (
|
||||
<Button color='primary' variant='contained' onClick={onSave} disabled={!position}>
|
||||
<Button
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={onSave}
|
||||
disabled={position == null}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color='primary'
|
||||
variant='contained'
|
||||
onClick={() => onClose()}
|
||||
disabled={!position}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
disabled={position == null}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
|
@ -1,126 +1,40 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
List,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import { EnableTelegramDialog } from '.';
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import { UserNinjaIcon } from '../Icons';
|
||||
|
||||
import { getWebln } from '../../utils';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { type Robot } from '../../models';
|
||||
import { signCleartextMessage } from '../../pgp';
|
||||
import RobotInfo from '../RobotInfo';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { Coordinator } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
robot: Robot;
|
||||
setRobot: (state: Robot) => void;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Props): JSX.Element => {
|
||||
const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element => {
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const [rewardInvoice, setRewardInvoice] = useState<string>('');
|
||||
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
|
||||
const [withdrawn, setWithdrawn] = useState<boolean>(false);
|
||||
const [badInvoice, setBadInvoice] = useState<string>('');
|
||||
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
|
||||
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
|
||||
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
|
||||
|
||||
const handleWebln = async () => {
|
||||
const webln = await getWebln()
|
||||
.then(() => {
|
||||
setWeblnEnabled(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setWeblnEnabled(false);
|
||||
console.log('WebLN not available');
|
||||
});
|
||||
return webln;
|
||||
};
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
handleWebln();
|
||||
}, []);
|
||||
|
||||
const handleWeblnInvoiceClicked = async (e: any) => {
|
||||
e.preventDefault();
|
||||
if (robot.earnedRewards) {
|
||||
const webln = await getWebln();
|
||||
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
|
||||
if (invoice) {
|
||||
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string) => {
|
||||
setBadInvoice('');
|
||||
setShowRewardsSpinner(true);
|
||||
signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then((signedInvoice) => {
|
||||
apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/reward/',
|
||||
{
|
||||
invoice: signedInvoice,
|
||||
},
|
||||
{ tokenSHA256: robot.tokenSHA256 },
|
||||
)
|
||||
.then((data: any) => {
|
||||
setBadInvoice(data.bad_invoice ?? '');
|
||||
setShowRewardsSpinner(false);
|
||||
setWithdrawn(data.successful_withdrawal);
|
||||
setOpenClaimRewards(!data.successful_withdrawal);
|
||||
setRobot({
|
||||
...robot,
|
||||
earnedRewards: data.successful_withdrawal ? 0 : robot.earnedRewards,
|
||||
});
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const setStealthInvoice = (wantsStealth: boolean) => {
|
||||
apiClient
|
||||
.post(baseUrl, '/api/stealth/', { wantsStealth }, { tokenSHA256: robot.tokenSHA256 })
|
||||
.then((data) => {
|
||||
setRobot({ ...robot, stealthInvoices: data?.wantsStealth });
|
||||
});
|
||||
};
|
||||
setLoading(garage.getRobot().loading);
|
||||
}, [robotUpdatedAt]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -129,7 +43,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
aria-labelledby='profile-title'
|
||||
aria-describedby='profile-description'
|
||||
>
|
||||
<div style={robot.loading ? {} : { display: 'none' }}>
|
||||
<div style={loading ? {} : { display: 'none' }}>
|
||||
<LinearProgress />
|
||||
</div>
|
||||
<DialogContent>
|
||||
@ -143,7 +57,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
<ListItem className='profileNickname'>
|
||||
<ListItemText secondary={t('Your robot')}>
|
||||
<Typography component='h6' variant='h6'>
|
||||
{robot.nickname ? (
|
||||
{garage.getRobot().nickname !== undefined && (
|
||||
<div style={{ position: 'relative', left: '-7px' }}>
|
||||
<div
|
||||
style={{
|
||||
@ -156,12 +70,12 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
>
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
|
||||
<a>{robot.nickname}</a>
|
||||
<a>{garage.getRobot().nickname}</a>
|
||||
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
@ -169,219 +83,35 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
<RobotAvatar
|
||||
avatarClass='profileAvatar'
|
||||
style={{ width: 65, height: 65 }}
|
||||
nickname={robot.nickname}
|
||||
nickname={garage.getRobot().nickname}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{robot.activeOrderId ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.activeOrderId}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent='' color='primary'>
|
||||
<NumbersIcon color='primary' />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
secondary={t('Your current order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : robot.lastOrderId ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.lastOrderId}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<NumbersIcon color='primary' />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
secondary={t('Inactive order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<NumbersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('No active orders')}
|
||||
secondary={t('You do not have previous orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<EnableTelegramDialog
|
||||
open={openEnableTelegram}
|
||||
onClose={() => {
|
||||
setOpenEnableTelegram(false);
|
||||
}}
|
||||
tgBotName={robot.tgBotName}
|
||||
tgToken={robot.tgToken}
|
||||
/>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SendIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
{robot.tgEnabled ? (
|
||||
<Typography color={theme.palette.success.main}>
|
||||
<b>{t('Telegram enabled')}</b>
|
||||
</Typography>
|
||||
) : (
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setOpenEnableTelegram(true);
|
||||
}}
|
||||
>
|
||||
{t('Enable Telegram Notifications')}
|
||||
</Button>
|
||||
)}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<UserNinjaIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
title={t(
|
||||
"Stealth lightning invoices do not contain details about the trade except an order reference. Enable this setting if you don't want to disclose details to a custodial lightning wallet.",
|
||||
)}
|
||||
>
|
||||
<Grid item>
|
||||
<FormControlLabel
|
||||
labelPlacement='end'
|
||||
label={t('Use stealth invoices')}
|
||||
control={
|
||||
<Switch
|
||||
checked={robot.stealthInvoices}
|
||||
onChange={() => {
|
||||
setStealthInvoice(!robot.stealthInvoices);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EmojiEventsIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
{!openClaimRewards ? (
|
||||
<ListItemText secondary={t('Your earned rewards')}>
|
||||
<Grid container>
|
||||
<Grid item xs={9}>
|
||||
<Typography>{`${robot.earnedRewards} Sats`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={3}>
|
||||
<Button
|
||||
disabled={robot.earnedRewards === 0}
|
||||
onClick={() => {
|
||||
setOpenClaimRewards(true);
|
||||
}}
|
||||
variant='contained'
|
||||
size='small'
|
||||
>
|
||||
{t('Claim')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItemText>
|
||||
) : (
|
||||
<form noValidate style={{ maxWidth: 270 }}>
|
||||
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<Grid item style={{ display: 'flex', maxWidth: 160 }}>
|
||||
<TextField
|
||||
error={!!badInvoice}
|
||||
helperText={badInvoice || ''}
|
||||
label={t('Invoice for {{amountSats}} Sats', {
|
||||
amountSats: robot.earnedRewards,
|
||||
})}
|
||||
size='small'
|
||||
value={rewardInvoice}
|
||||
onChange={(e) => {
|
||||
setRewardInvoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 80 }}>
|
||||
<Button
|
||||
sx={{ maxHeight: 38 }}
|
||||
onClick={(e) => {
|
||||
handleSubmitInvoiceClicked(e, rewardInvoice);
|
||||
}}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
type='submit'
|
||||
>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{weblnEnabled ? (
|
||||
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 240 }}>
|
||||
<Button
|
||||
sx={{ maxHeight: 38, minWidth: 230 }}
|
||||
onClick={async (e) => {
|
||||
await handleWeblnInvoiceClicked(e);
|
||||
}}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
type='submit'
|
||||
>
|
||||
{t('Generate with Webln')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{showRewardsSpinner && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{withdrawn && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography color='primary' variant='body2'>
|
||||
<b>{t('There it goes, thank you!🥇')}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<Typography>
|
||||
<b>{t('Coordinators that know your robots')}</b>
|
||||
</Typography>
|
||||
|
||||
{Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => {
|
||||
if (garage.getRobot()?.loading === false) {
|
||||
return (
|
||||
<div key={coordinator.shortAlias}>
|
||||
<RobotInfo
|
||||
coordinator={coordinator}
|
||||
robot={garage.getRobot()}
|
||||
slotIndex={garage.currentSlot}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div key={coordinator.shortAlias} />;
|
||||
}
|
||||
})}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -1,225 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Divider,
|
||||
Link,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import WebIcon from '@mui/icons-material/Web';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import EqualizerIcon from '@mui/icons-material/Equalizer';
|
||||
|
||||
import { AmbossIcon, BitcoinSignIcon, RoboSatsNoTextIcon } from '../Icons';
|
||||
|
||||
import { pn } from '../../utils';
|
||||
import { type Info } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
info: Info;
|
||||
}
|
||||
|
||||
const StatsDialog = ({ open = false, onClose, info }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isOnionAccess = window.location.hostname.endsWith('.onion');
|
||||
|
||||
const ambossURL = isOnionAccess
|
||||
? `http://amboss5jfdzzblty5dr5zaig5twvkgsla6y5xuy6s5c5ogpjfcqgltid.onion/node/${info.node_id}`
|
||||
: `https://amboss.space/node/${info.node_id}`;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<div style={info.loading ? {} : { display: 'none' }}>
|
||||
<LinearProgress />
|
||||
</div>
|
||||
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Stats For Nerds')}
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<RoboSatsNoTextIcon
|
||||
sx={{ width: '1.4em', height: '1.4em', right: '0.2em', position: 'relative' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${t('Client')} ${info.clientVersion} - ${t('Coordinator')} ${
|
||||
info.coordinatorVersion
|
||||
}`}
|
||||
secondary={t('RoboSats version')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{info.lnd_version ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<BoltIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={info.lnd_version} secondary={t('LND version')} />
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
{info.cln_version ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<BoltIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={info.cln_version} secondary={t('CLN version')} />
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
<Divider />
|
||||
|
||||
{info.network === 'testnet' ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<DnsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={`${t('LN Node')}: ${info.node_alias}`}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://1ml.com/testnet/node/${info.node_id}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${info.node_id.slice(0, 12)}... (1ML)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AmbossIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={info.node_alias}>
|
||||
<Link target='_blank' href={ambossURL} rel='noreferrer'>
|
||||
{`${info.node_id.slice(0, 12)}... (AMBOSS)`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<WebIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={info.alternative_name}>
|
||||
<Link target='_blank' href={`http://${info.alternative_site}`} rel='noreferrer'>
|
||||
{`${info.alternative_site.slice(0, 12)}...onion`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<GitHubIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Coordinator commit hash')}>
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`https://github.com/RoboSats/robosats/tree/${info.robosats_running_commit_hash}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
{`${info.robosats_running_commit_hash.slice(0, 12)}...`}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EqualizerIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('24h contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(info.last_day_volume)}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EqualizerIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Lifetime contracted volume')}>
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pn(info.lifetime_volume)}
|
||||
<BitcoinSignIcon sx={{ width: 14, height: 14 }} color={'text.secondary'} />
|
||||
</div>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PublicIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'left',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<span>{`${t('Made with')} `}</span>
|
||||
<FavoriteIcon sx={{ color: '#ff0000', height: '22px', width: '22px' }} />
|
||||
<span>{` ${t('and')} `}</span>
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '23px', width: '23px' }} />
|
||||
</div>
|
||||
}
|
||||
secondary={t('... somewhere on Earth!')}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatsDialog;
|
@ -14,7 +14,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { systemClient } from '../../services/System';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@ -24,7 +24,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => {
|
||||
const { robot } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -43,7 +43,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
|
||||
sx={{ width: '100%', maxWidth: '550px' }}
|
||||
disabled
|
||||
label={t('Back it up!')}
|
||||
value={robot.token}
|
||||
value={garage.getRobot().token}
|
||||
variant='filled'
|
||||
size='small'
|
||||
InputProps={{
|
||||
@ -51,7 +51,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(robot.token);
|
||||
systemClient.copyToClipboard(garage.getRobot().token);
|
||||
}}
|
||||
>
|
||||
<ContentCopy color='primary' />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@ -17,24 +17,32 @@ import {
|
||||
import WebIcon from '@mui/icons-material/Web';
|
||||
import AndroidIcon from '@mui/icons-material/Android';
|
||||
import UpcomingIcon from '@mui/icons-material/Upcoming';
|
||||
import { checkVer } from '../../utils';
|
||||
import { type Version } from '../../models';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
clientVersion: string;
|
||||
coordinatorVersion: string;
|
||||
coordinatorVersion: Version;
|
||||
clientVersion: Version;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UpdateClientDialog = ({
|
||||
open = false,
|
||||
clientVersion,
|
||||
coordinatorVersion,
|
||||
onClose,
|
||||
}: Props): JSX.Element => {
|
||||
const UpdateDialog = ({ coordinatorVersion, clientVersion }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState<boolean>(() => checkVer(coordinatorVersion));
|
||||
const coordinatorString = `v${coordinatorVersion.major}-${coordinatorVersion.minor}-${coordinatorVersion.patch}`;
|
||||
const clientString = `v${clientVersion.major}-${clientVersion.minor}-${clientVersion.patch}`;
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(checkVer(coordinatorVersion));
|
||||
}, [coordinatorVersion]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Update your RoboSats client')}
|
||||
@ -45,7 +53,7 @@ const UpdateClientDialog = ({
|
||||
<Typography>
|
||||
{t(
|
||||
'The RoboSats coordinator is on version {{coordinatorVersion}}, but your client app is {{clientVersion}}. This version mismatch might lead to a bad user experience.',
|
||||
{ coordinatorVersion, clientVersion },
|
||||
{ coordinatorString, clientString },
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
@ -53,7 +61,7 @@ const UpdateClientDialog = ({
|
||||
<ListItemButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorVersion}-alpha`}
|
||||
href={`https://github.com/RoboSats/robosats/releases/tag/${coordinatorString}-alpha`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<ListItemIcon>
|
||||
@ -107,7 +115,13 @@ const UpdateClientDialog = ({
|
||||
</ListItemButton>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('Go away!')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('Go away!')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</List>
|
||||
</DialogContent>
|
||||
@ -115,4 +129,4 @@ const UpdateClientDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateClientDialog;
|
||||
export default UpdateDialog;
|
@ -1,14 +1,15 @@
|
||||
export { default as AuditPGPDialog } from './AuditPGP';
|
||||
export { default as CommunityDialog } from './Community';
|
||||
export { default as InfoDialog } from './Info';
|
||||
export { default as AboutDialog } from './About';
|
||||
export { default as LearnDialog } from './Learn';
|
||||
export { default as NoRobotDialog } from './NoRobot';
|
||||
export { default as StoreTokenDialog } from './StoreToken';
|
||||
export { default as ConfirmationDialog } from './Confirmation';
|
||||
export { default as CoordinatorSummaryDialog } from './CoordinatorSummary';
|
||||
export { default as ExchangeDialog } from './Exchange';
|
||||
export { default as CoordinatorDialog } from './Coordinator';
|
||||
export { default as ProfileDialog } from './Profile';
|
||||
export { default as StatsDialog } from './Stats';
|
||||
export { default as ClientDialog } from './Client';
|
||||
export { default as EnableTelegramDialog } from './EnableTelegram';
|
||||
export { default as UpdateClientDialog } from './UpdateClient';
|
||||
export { default as NoticeDialog } from './Notice';
|
||||
export { default as F2fMapDialog } from './F2fMap';
|
||||
export { default as UpdateDialog } from './Update';
|
||||
|
@ -30,7 +30,7 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ overflow: 'auto', height: '100%', width: '100%', background: 'white' }}>
|
||||
|
250
frontend/src/components/FederationTable/index.tsx
Normal file
250
frontend/src/components/FederationTable/index.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
import React, { useCallback, useEffect, useState, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, useTheme, Checkbox, CircularProgress, Typography, Grid } from '@mui/material';
|
||||
import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid';
|
||||
import { type Coordinator } from '../../models';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { Link, LinkOff } from '@mui/icons-material';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
import {
|
||||
ActionFederation,
|
||||
UseFederationStoreType,
|
||||
FederationContext,
|
||||
} from '../../contexts/FederationContext';
|
||||
|
||||
interface FederationTableProps {
|
||||
federation: Record<string, Coordinator>;
|
||||
dispatchFederation: (action: ActionFederation) => void;
|
||||
fetchCoordinatorInfo: (coordinator: Coordinator) => Promise<void>;
|
||||
setFocusedCoordinator: (state: number) => void;
|
||||
openCoordinator: () => void;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
fillContainer?: boolean;
|
||||
}
|
||||
|
||||
const FederationTable = ({
|
||||
openCoordinator,
|
||||
maxWidth = 90,
|
||||
maxHeight = 50,
|
||||
fillContainer = false,
|
||||
}: FederationTableProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { federation, setFocusedCoordinator } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const theme = useTheme();
|
||||
const [pageSize, setPageSize] = useState<number>(0);
|
||||
|
||||
// all sizes in 'em'
|
||||
const fontSize = theme.typography.fontSize;
|
||||
const verticalHeightFrame = 3.25;
|
||||
const verticalHeightRow = 3.25;
|
||||
const defaultPageSize = Math.max(
|
||||
Math.floor((maxHeight - verticalHeightFrame) / verticalHeightRow),
|
||||
1,
|
||||
);
|
||||
const height = defaultPageSize * verticalHeightRow + verticalHeightFrame;
|
||||
|
||||
const [useDefaultPageSize, setUseDefaultPageSize] = useState(true);
|
||||
useEffect(() => {
|
||||
if (useDefaultPageSize) {
|
||||
setPageSize(defaultPageSize);
|
||||
}
|
||||
});
|
||||
|
||||
const localeText = {
|
||||
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
|
||||
noResultsOverlayLabel: t('No coordinators found.'),
|
||||
};
|
||||
|
||||
const onClickCoordinator = function (shortAlias: string): void {
|
||||
setFocusedCoordinator(shortAlias);
|
||||
openCoordinator();
|
||||
};
|
||||
|
||||
const aliasObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'longAlias',
|
||||
headerName: t('Coordinator'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction='row'
|
||||
sx={{ cursor: 'pointer', position: 'relative', left: '-0.3em', width: '50em' }}
|
||||
wrap='nowrap'
|
||||
onClick={() => {
|
||||
onClickCoordinator(params.row.shortAlias);
|
||||
}}
|
||||
alignItems='center'
|
||||
spacing={1}
|
||||
>
|
||||
<Grid item>
|
||||
<RobotAvatar
|
||||
nickname={params.row.shortAlias}
|
||||
coordinator={true}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
baseUrl={hostUrl}
|
||||
small={true}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography>{params.row.longAlias}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const enabledObj = useCallback(
|
||||
(width: number) => {
|
||||
return {
|
||||
field: 'enabled',
|
||||
headerName: t('Enabled'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={params.row.enabled}
|
||||
onClick={() => {
|
||||
onEnableChange(params.row.shortAlias);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[federation],
|
||||
);
|
||||
|
||||
const upObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'up',
|
||||
headerName: t('Up'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onClickCoordinator(params.row.shortAlias);
|
||||
}}
|
||||
>
|
||||
{Boolean(params.row.loadingInfo) && Boolean(params.row.enabled) ? (
|
||||
<CircularProgress thickness={0.35 * fontSize} size={1.5 * fontSize} />
|
||||
) : params.row.info !== undefined ? (
|
||||
<Link color='success' />
|
||||
) : (
|
||||
<LinkOff color='error' />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const columnSpecs = {
|
||||
alias: {
|
||||
priority: 2,
|
||||
order: 1,
|
||||
normal: {
|
||||
width: 12.1,
|
||||
object: aliasObj,
|
||||
},
|
||||
},
|
||||
up: {
|
||||
priority: 3,
|
||||
order: 2,
|
||||
normal: {
|
||||
width: 3.5,
|
||||
object: upObj,
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
priority: 1,
|
||||
order: 3,
|
||||
normal: {
|
||||
width: 5,
|
||||
object: enabledObj,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const filteredColumns = function (): {
|
||||
columns: Array<GridColDef<GridValidRowModel>>;
|
||||
width: number;
|
||||
} {
|
||||
const useSmall = maxWidth < 30;
|
||||
const selectedColumns: object[] = [];
|
||||
let width: number = 0;
|
||||
|
||||
for (const value of Object.values(columnSpecs)) {
|
||||
const colWidth = Number(
|
||||
useSmall && Boolean(value.small) ? value.small.width : value.normal.width,
|
||||
);
|
||||
const colObject = useSmall && Boolean(value.small) ? value.small.object : value.normal.object;
|
||||
|
||||
if (width + colWidth < maxWidth || selectedColumns.length < 2) {
|
||||
width = width + colWidth;
|
||||
selectedColumns.push([colObject(colWidth, false), value.order]);
|
||||
} else {
|
||||
selectedColumns.push([colObject(colWidth, true), value.order]);
|
||||
}
|
||||
}
|
||||
|
||||
// sort columns by column.order value
|
||||
selectedColumns.sort(function (first, second) {
|
||||
return first[1] - second[1];
|
||||
});
|
||||
|
||||
const columns: Array<GridColDef<GridValidRowModel>> = selectedColumns.map(function (item) {
|
||||
return item[0];
|
||||
});
|
||||
|
||||
return { columns, width: width * 0.9 };
|
||||
};
|
||||
|
||||
const { columns, width } = filteredColumns();
|
||||
|
||||
const onEnableChange = function (shortAlias: string): void {
|
||||
if (federation.getCoordinator(shortAlias).enabled) {
|
||||
federation.disbaleCoordinator(shortAlias);
|
||||
} else {
|
||||
federation.enaleCoordinator(shortAlias);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={
|
||||
fillContainer
|
||||
? { width: '100%', height: '100%' }
|
||||
: { width: `${width}em`, height: `${height}em`, overflow: 'auto' }
|
||||
}
|
||||
>
|
||||
<DataGrid
|
||||
localeText={localeText}
|
||||
rowHeight={3.714 * theme.typography.fontSize}
|
||||
headerHeight={3.25 * theme.typography.fontSize}
|
||||
rows={Object.values(federation.coordinators)}
|
||||
getRowId={(params: Coordinator) => params.shortAlias}
|
||||
columns={columns}
|
||||
checkboxSelection={false}
|
||||
pageSize={pageSize}
|
||||
rowsPerPageOptions={width < 22 ? [] : [0, pageSize, defaultPageSize * 2, 50, 100]}
|
||||
onPageSizeChange={(newPageSize) => {
|
||||
setPageSize(newPageSize);
|
||||
setUseDefaultPageSize(false);
|
||||
}}
|
||||
hideFooter={true}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FederationTable;
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Paper, Alert, AlertTitle, Button, Link } from '@mui/material';
|
||||
import { Paper, Alert, AlertTitle, Button } from '@mui/material';
|
||||
|
||||
const SelfhostedAlert = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -12,7 +12,7 @@ const UnsafeAlert = (): JSX.Element => {
|
||||
const [unsafeClient, setUnsafeClient] = useState<boolean>(false);
|
||||
|
||||
// To do. Read from Coordinators Obj.
|
||||
const safe_urls = [
|
||||
const safeUrls = [
|
||||
'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion',
|
||||
'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion',
|
||||
'robodevs7ixniseezbv7uryxhamtz3hvcelzfwpx3rvoipttjomrmpqd.onion',
|
||||
@ -20,11 +20,8 @@ const UnsafeAlert = (): JSX.Element => {
|
||||
'r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p',
|
||||
];
|
||||
|
||||
const checkClient = () => {
|
||||
const http = new XMLHttpRequest();
|
||||
const h = getHost();
|
||||
const unsafe = !safe_urls.includes(h);
|
||||
|
||||
const checkClient = (): void => {
|
||||
const unsafe = !safeUrls.includes(getHost());
|
||||
setUnsafeClient(unsafe);
|
||||
};
|
||||
|
||||
@ -106,6 +103,8 @@ const UnsafeAlert = (): JSX.Element => {
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import SelfhostedAlert from './SelfhostedAlert';
|
||||
import UnsafeAlert from './UnsafeAlert';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function AmbossIcon(props) {
|
||||
const AmbossIcon: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 95.7 84.9'>
|
||||
<g id='Layer_2_00000052094167160547307180000012226084410257483709_'>
|
||||
@ -25,4 +25,6 @@ export default function AmbossIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AmbossIcon;
|
||||
|
152
frontend/src/components/Icons/BadgeDevFund.tsx
Normal file
152
frontend/src/components/Icons/BadgeDevFund.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const BadgeDevFund: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} viewBox='0 0 100 100'>
|
||||
<g>
|
||||
<path
|
||||
fill='#7BD3FF'
|
||||
d='M100,12.893C100,5.535,93.686,0,86.33,0H13.44C6.085,0,0,5.535,0,12.893v72.879
|
||||
C0,93.129,6.085,100,13.44,100h72.89c7.355,0,13.67-6.871,13.67-14.229V12.893z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='none'
|
||||
d='M60.172,65H38.148c-0.824,0-1.2,0.006-1.2,0.5c0,0.555,0.433,0.5,0.944,0.5h22.024
|
||||
c0.514,0,0.946,0.055,0.946-0.5C60.863,65.053,60.528,65,60.172,65z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M57.228,27.309l-5.355-0.375l-4.449-0.312l-1.681-0.117L41.5,27.358l-9.062,7.438
|
||||
c-0.028,0.057-0.068,0.109-0.12,0.152c-0.022,0.018-0.046,0.033-0.068,0.047l-2.752,11.648c0.317,0.322,1.107,1.011,1.999,0.887
|
||||
c0.912-0.126,1.784-1.099,2.522-2.813c0.486-1.128,0.922-2.109,1.309-2.961c0.037,0.284,0.082,0.565,0.136,0.845
|
||||
c0.051,0.26,0.106,0.518,0.171,0.772c1.481,5.843,6.773,10.167,13.078,10.167c7.449,0,13.49-6.041,13.49-13.49
|
||||
c0-0.873-0.084-1.725-0.243-2.55l-0.689,0.233l-0.692,0.232c0.12,0.678,0.182,1.373,0.182,2.084
|
||||
c0,6.643-5.404,12.047-12.047,12.047c-6.315,0-11.342-4.883-11.842-11.07C36.846,40.705,37,40.379,37,40.051
|
||||
c0-0.013,0-0.025,0-0.039c0-0.404-0.145-0.805-0.105-1.199c0.621-6.062,5.677-10.81,11.902-10.81
|
||||
c2.838,0,5.406,0.987,7.469,2.636l0.202-0.74l0.213-0.74L57.228,27.309z'
|
||||
/>
|
||||
|
||||
<rect
|
||||
x='41.968'
|
||||
y='43.084'
|
||||
transform='matrix(0.9718 0.2357 -0.2357 0.9718 11.7218 -9.985)'
|
||||
fill='#0275BD'
|
||||
width='11.31'
|
||||
height='1.9'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M42.923,39.859l5.267,1.277c-0.005-0.029-0.01-0.059-0.015-0.088c-0.139-1.016,0.423-1.721,0.445-1.749
|
||||
l0.01-0.012l-5.259-1.276L42.923,39.859z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M53.914,42.526l0.065-0.272c-0.133,0.068-0.263,0.133-0.39,0.195L53.914,42.526z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M43.863,36.33l6.238,1.514c0.055-0.053,0.11-0.106,0.168-0.163c0.441-0.427,0.937-0.906,1.445-1.401
|
||||
l-7.402-1.795L43.863,36.33z'
|
||||
/>
|
||||
|
||||
<rect
|
||||
x='45.277'
|
||||
y='46.947'
|
||||
transform='matrix(-0.2448 0.9696 -0.9696 -0.2448 104.3776 14.2907)'
|
||||
fill='#0275BD'
|
||||
width='2.692'
|
||||
height='1.692'
|
||||
/>
|
||||
<polygon
|
||||
fill='#0275BD'
|
||||
points='50.902,34.197 51.019,33.735 51.136,33.274 51.532,31.698 49.893,31.285 49.435,33.104
|
||||
49.317,33.565 49.235,33.895 50.134,34.121 50.874,34.308 '
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M71.156,32.678l4.156-8.516c-0.048-0.015-0.096-0.029-0.142-0.049l-0.012,0.039l-0.789-0.244l-8.853-2.74
|
||||
c-1.147,0.99-2.557,2.221-3.837,3.354c-0.119,0.107-0.239,0.215-0.357,0.32c-0.123,0.109-0.244,0.217-0.362,0.322
|
||||
c-1.068,0.954-1.973,1.781-2.448,2.256c-0.175,0.175-0.294,0.01-0.338,0.075c0.001-0.002-0.056,0.001-0.099,0.019l-0.611,1.979
|
||||
l-0.23,0.91l-0.231,0.842l-0.402,1.37l0.148-0.073l0.777,1.258l0.301,0.487l0.303,0.487l1.556,2.5l0.693-0.232l0.685-0.232
|
||||
l0.687-0.23l5.118-1.729C69.259,34.021,70.797,32.943,71.156,32.678z M66.1,26.309c-0.34,0-0.646-0.146-0.858-0.379
|
||||
c-0.193-0.209-0.312-0.486-0.312-0.791c0-0.646,0.524-1.17,1.17-1.17s1.169,0.523,1.169,1.17c0,0.123-0.019,0.242-0.055,0.354
|
||||
C67.063,25.965,66.622,26.309,66.1,26.309z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M26.862,36.773c0.965,0,2.087-0.457,3.338-1.357l1.13-0.927l-2.778-3.38
|
||||
c-0.091-0.109-0.121-0.247-0.1-0.376l-4.283,4.135C24.44,35.45,25.235,36.773,26.862,36.773z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M31.657,33.404l0.376,0.457l0.024,0.03l0.421-0.345l3.94-3.234l-1.29-1.811l-1.778-2.498l-4.496,4.341
|
||||
c0.156-0.021,0.317,0.036,0.424,0.166L31.657,33.404z'
|
||||
/>
|
||||
<polygon
|
||||
fill='#0275BD'
|
||||
points='37.148,29.713 40.781,26.729 40.08,22.621 39.996,22.131 39.912,21.641 39.623,19.949
|
||||
34.036,25.342 35.66,27.624 '
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M46.396,19.289l0.491,5.988L48,25.604c0,0.013,0,0.024,0,0.036l10.742,0.791
|
||||
c0.473-0.459,0.915-1.121,2.006-2.096c0.113-0.101,0.094-0.205,0.215-0.312c0.113-0.102,0.161-0.204,0.281-0.311
|
||||
c0.032-0.029,0.028-0.055,0.061-0.084c1.894-1.676,3.794-3.328,3.814-3.345l0.188-0.171l9.377,2.905l-8.331-9.662l-0.448,0.257
|
||||
l-0.077,0.016c-2.756,0.532-12.898,2.773-19.544,4.252l0.073,0.924L46.396,19.289z M64.687,15.846c0.646,0,1.17,0.523,1.17,1.17
|
||||
c0,0.646-0.524,1.168-1.17,1.168s-1.169-0.522-1.169-1.168C63.518,16.369,64.041,15.846,64.687,15.846z M48.854,18.955
|
||||
c0.215-0.245,0.529-0.401,0.881-0.401c0.338,0,0.641,0.144,0.854,0.372c0.194,0.209,0.315,0.488,0.315,0.797
|
||||
c0,0.646-0.524,1.17-1.17,1.17s-1.169-0.523-1.169-1.17C48.565,19.43,48.675,19.16,48.854,18.955z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M40.871,21.658l0.084,0.49l0.719,4.215l4.008-0.809l0.272,0.02l-0.439-5.49L45.476,19.6l-0.039-0.484
|
||||
l-0.082-1.029c-2.497,0.557-4.357,0.973-4.905,1.096l0.339,1.986L40.871,21.658z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M49.348,39.898c-0.037,0.051-0.323,0.459-0.237,1.039c0.022,0.153,0.068,0.306,0.139,0.457
|
||||
c0.186,0.405,0.536,0.805,1.051,1.193c0.268-0.041,0.873-0.166,1.764-0.51c0.35-0.135,0.742-0.304,1.176-0.515
|
||||
c-0.396-0.544-0.732-1.007-1.019-1.403c-0.659-0.915-1.05-1.473-1.283-1.816c-0.004,0.006-0.008,0.01-0.013,0.014
|
||||
c-0.47,0.455-0.924,0.895-1.225,1.189C49.518,39.727,49.39,39.854,49.348,39.898z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M54.281,41.015c1.28-0.724,2.82-1.774,4.57-3.298l-1.87-3.002l-0.306-0.491l-0.307-0.491l-0.291-0.467
|
||||
c-0.121,0.127-0.254,0.266-0.399,0.414c-0.134,0.139-0.278,0.285-0.43,0.438c-0.14,0.141-0.284,0.287-0.436,0.438
|
||||
c-0.618,0.617-1.322,1.308-2.018,1.986c-0.399,0.39-0.797,0.774-1.172,1.14c0.105,0.157,0.258,0.377,0.442,0.639
|
||||
c0.41,0.582,0.979,1.372,1.567,2.182c0.149,0.208,0.301,0.416,0.452,0.623C54.15,41.089,54.216,41.051,54.281,41.015z'
|
||||
/>
|
||||
<polygon
|
||||
fill='#0275BD'
|
||||
points='79.825,16.213 69.762,10.616 67.608,12.816 75.157,21.717 '
|
||||
/>
|
||||
<polygon
|
||||
fill='#0275BD'
|
||||
points='70.018,9.68 79.255,14.817 80.446,15.48 80.866,15.714 81.285,15.947 81.969,16.328
|
||||
83.609,12.775 70.943,5.73 69.304,9.283 69.591,9.443 '
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M81.919,91.473c0.007-0.013,0.014-0.025,0.021-0.038c-0.006-0.017-0.01-0.036-0.015-0.055
|
||||
C81.923,91.41,81.921,91.44,81.919,91.473z'
|
||||
/>
|
||||
<path
|
||||
fill='#0275BD'
|
||||
d='M77.68,59.103c-0.064-0.093-54.519-0.051-54.519-0.051L14,73.543V91.86v0.251v0.061
|
||||
c0,0.013,0.137,0.025,0.138,0.038c0.002,0.004,0.069,0.041,0.073,0.044c0.051,0.4,0.448,0.746,0.851,0.746h0.016
|
||||
C15.514,93,17,92.563,17,92.111V91.86c0-0.015-0.001-0.029-0.002-0.044C16.999,91.815,17,91.813,17,91.812V75h65v16.86v0.229
|
||||
v0.021C82,92.563,82.178,93,82.614,93h0.016c0.437,0,1.37-0.437,1.37-0.889v-0.002V91.86V73.235
|
||||
C84,72.922,77.68,59.103,77.68,59.103z M59.917,66H37.893c-0.512,0-0.944,0.055-0.944-0.5c0-0.494,0.376-0.5,1.2-0.5h22.023
|
||||
c0.356,0,0.691,0.053,0.691,0.5C60.863,66.055,60.431,66,59.917,66z'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeDevFund;
|
79
frontend/src/components/Icons/BadgeFounder.tsx
Normal file
79
frontend/src/components/Icons/BadgeFounder.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const BadgeFounder: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} viewBox='0 0 100 100'>
|
||||
<g>
|
||||
<path
|
||||
fill='#DF87FF'
|
||||
d='M100,12.543C100,5.194,93.416,0,86.036,0H13.239C5.858,0,0,5.194,0,12.543v72.912
|
||||
C0,92.804,5.858,100,13.239,100h72.797C93.416,100,100,92.804,100,85.455V12.543z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='#9517CE'
|
||||
d='M47.421,31.641l0.003-0.967c0.009-1.817-1.459-3.296-3.275-3.303l-11.994-0.05
|
||||
c-1.816-0.007-3.296,1.46-3.304,3.276l-0.004,0.965c-0.008,1.818,1.459,3.297,3.277,3.305l11.991,0.049
|
||||
C45.934,34.924,47.413,33.456,47.421,31.641z'
|
||||
/>
|
||||
<path
|
||||
fill='#9517CE'
|
||||
d='M70,45.531v5.008l2.57-4.386c0.43,0.041,0.66,0.063,1.104,0.063c7.59,0,13.642-6.154,13.642-13.746
|
||||
c0-7.59-6.206-13.743-13.797-13.743s-13.357,6.153-13.357,13.743C60.161,38.563,65,43.729,70,45.531z M71.773,41.121l-1.619-0.407
|
||||
l0.649-2.58l1.62,0.409L71.773,41.121z M74.52,23.316l1.619,0.41l-0.65,2.577l-1.62-0.407L74.52,23.316z M69.005,26.477
|
||||
l10.859,2.637l-0.443,1.824l-10.858-2.635L69.005,26.477z M68.075,29.965L78.935,32.6l-0.443,1.824l-10.858-2.635L68.075,29.965z
|
||||
M67.067,33.683l10.86,2.636l-0.444,1.825l-10.858-2.636L67.067,33.683z'
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill='#9517CE'
|
||||
d='M90.127,83.767c-0.516-0.809-0.598-1.344-1.545-1.613L89,83.285V83.18L87.997,82c0.001,0-0.034,0-0.034,0
|
||||
l2.545-11.693c0-4.914-4.002-8.307-8.917-8.307H58.618c-3.943-2-7.946-4.107-11.954-4.908c-0.894-0.18-1.787-0.582-2.682-0.693
|
||||
l-0.006,1.05l-0.004,1.604C46.879,59.443,49.97,60,53.21,62h-1.782c-4.914,0-8.292,2.769-8.897,8.406L40.29,82H22.418h-0.454
|
||||
l-0.011-5.766c3.257-1.551,5.418-4.831,5.434-8.609c0.01-2.396-0.855-4.6-2.303-6.289c2.21-1.017,4.907-1.982,8.001-2.573
|
||||
l-0.01,2.682c-0.01,2.455,1.979,4.505,4.435,4.516L39.49,66h0.019c2.444,0,4.441-2.021,4.451-4.467l0.013-3.036l0.004-1.173
|
||||
l0.006-1.177l0.01-2.543c1.155-0.467,2.245-1.091,3.229-1.862c2.458-1.925,4.144-4.577,4.873-7.701
|
||||
c2.039-8.738,2.42-27.79-9.423-30.905c-0.535-0.141-1.123-0.241-1.757-0.298l0.017-4.204c0.001-0.052-0.001-0.104-0.002-0.154
|
||||
c0.62-0.569,1.009-1.387,1.009-2.295c0-1.719-1.394-3.112-3.112-3.112s-3.112,1.394-3.112,3.112c0,0.808,0.308,1.543,0.812,2.096
|
||||
c-0.009,0.11-0.016,0.222-0.016,0.336l-0.017,4.195c-0.671,0.049-1.292,0.145-1.853,0.285c-11.868,3.02-11.645,22.07-9.677,30.826
|
||||
c1.017,4.521,4.113,8.062,8.146,9.702l-0.011,2.784c-0.16,0.029-0.319,0.056-0.479,0.086c-3.233,0.619-6.439,2.215-9.396,3.664
|
||||
C21.686,59.105,19.827,59,17.825,59h-0.04c-5.282,0-9.598,3.76-9.619,9.043c-0.013,3.072,1.427,5.592,3.668,7.36L11.806,82h-1.646
|
||||
c-1.72,0-2.982,0.588-3.752,1.797c-0.572,0.896-0.839,2.066-0.839,3.706c0,1.639,0.267,2.799,0.839,3.696
|
||||
C7.177,92.407,8.439,93,10.159,93h76.216c1.721,0,2.983-0.598,3.752-1.805c0.573-0.898,0.839-2.074,0.839-3.713
|
||||
S90.7,84.664,90.127,83.767z M42,61.489c0,1.17-1.14,2.122-2.31,2.122l-1.779-0.008C36.737,63.598,36,62.639,36,61.464v-0.172
|
||||
c1,0.363,1.648,0.784,3.059,0.784c0.898,0,1.941-0.171,2.941-0.637V61.489z M42,58.233l-0.182,1.609
|
||||
c-0.329,0.164-0.74,0.299-1.048,0.405c-1.828,0.633-3.298,0.343-4.178-0.017c-0.609-0.248-0.979-0.528-1.019-0.561
|
||||
c-0.045-0.038,0.025-0.724-0.026-0.749l0.031-1.297L36,57.664c0,0.206,0.605,0.43,1.234,0.585c0.486,0.121,0.891,0.2,1.504,0.2
|
||||
c0.604,0,1.367-0.077,2.082-0.27c0.349-0.095,1.18-0.215,1.18-0.367V58.233z M41.661,55.924l-0.002,0.291
|
||||
c-0.331,0.164-0.652,0.3-0.961,0.405c-1.828,0.633-3.255,0.343-4.137-0.017c-0.553-0.225-0.892-0.477-0.978-0.546
|
||||
c-0.009-0.007-0.015-0.012-0.019-0.015c-0.045-0.038-0.093-0.068-0.144-0.094l0.007-1.616c0.91,0.19,1.852,0.292,2.815,0.292
|
||||
c0.081,0,0.161-0.03,0.242-0.031c0.117,0.003,0.236-0.034,0.354-0.034l0,0c0.955,0,1.901-0.079,2.827-0.272L41.661,55.924z
|
||||
M38.368,9.265c0.149,0.022,0.303,0.033,0.458,0.033c0.082,0,0.162-0.004,0.241-0.011l-0.014,3.504
|
||||
c-0.131,0.004-0.263,0.009-0.396,0.016c-0.102-0.006-0.202-0.01-0.305-0.014L38.368,9.265z M31.256,49.9
|
||||
c-2.044-1.615-3.437-3.856-4.026-6.481c-1.735-7.716-2.132-25.495,7.983-28.068c0.375-0.095,0.805-0.163,1.27-0.206
|
||||
c0.297-0.025,0.609-0.043,0.932-0.047c0.303-0.004,0.613,0.002,0.931,0.018c0.077,0.004,0.155,0.009,0.232,0.014l0.071,0.004
|
||||
l0.073-0.004c0.107-0.005,0.216-0.01,0.322-0.015c0.317-0.011,0.629-0.013,0.931-0.004c0.323,0.009,0.635,0.03,0.931,0.062
|
||||
c0.429,0.047,0.825,0.115,1.176,0.208c10.094,2.655,9.55,20.431,7.752,28.132c-0.606,2.605-2.006,4.824-4.044,6.421
|
||||
c-0.561,0.439-1.165,0.821-1.799,1.145c-0.374,0.19-0.758,0.36-1.15,0.508c-0.997,0.374-2.004,0.799-3.064,0.939
|
||||
c-0.76,0.101-3.221-0.096-3.462-0.141c-0.71-0.131-1.361-0.363-2.035-0.614c-0.393-0.146-0.777-0.401-1.15-0.591
|
||||
C32.467,50.844,31.839,50.361,31.256,49.9z M87.689,83.287L87.689,83.287l-0.012,0.055L87.689,83.287z M61.314,68h0.438h1.163
|
||||
h1.161h7.311c1.37,0,1.305,1.201,1.305,2c0,0.801,0.065,2-1.305,2h-6.569h-0.863h-0.298h-0.863h-1.162h-0.316
|
||||
c-0.278,0-0.497,0-0.669,0h-0.047C60.046,72,60,70.252,60,69.725c0-0.066,0-0.259,0-0.325C60,68.6,59.944,68,61.314,68z M17.475,82
|
||||
h-3.346l0.007-1.979c0.674,0.299,1.604,0.56,2.743,0.56c0.222,0,0.449-0.02,0.686-0.043c0.642-0.061,1.34-0.183,2.086-0.462
|
||||
L19.642,82H17.475z M14.143,78.358l-0.117-1.896C15.136,76.916,16.447,77,17.719,77h0.023h0.046c0.642,0,1.311-0.254,1.917-0.373
|
||||
l-0.049,2.059c-0.229,0.104-0.452,0.082-0.67,0.158c-0.471,0.162-0.914,0.206-1.327,0.262c-1.2,0.16-2.157-0.097-2.812-0.363
|
||||
C14.547,78.62,14.311,78.464,14.143,78.358z M10.488,67.889C10.504,63.881,13.778,61,17.785,61h0.029
|
||||
c1.068,0,2.08-0.117,2.994,0.297c0.41,0.186,0.8,0.232,1.165,0.489c0.343,0.239,0.664,0.419,0.96,0.713
|
||||
c1.333,1.324,2.157,3.116,2.149,5.144c-0.011,2.439-1.229,4.57-3.084,5.881c-1.422,1.005-3.479,1.598-5.262,1.443
|
||||
c-0.509-0.044-1.47-0.17-1.94-0.374c-0.873-0.38-1.078-0.617-1.793-1.237c-0.433-0.375-0.821-0.771-1.155-1.236
|
||||
C10.987,70.919,10.48,69.479,10.488,67.889z M86.375,91H10.159c-1.471,0-2.268,0.203-2.268-2.5c0-2.086,0.361-2.5,2.268-2.5h1.638
|
||||
h0.733h0.428h1.16h1.839h1.369h0.464h1.843h1.161h1.161h0.463h17.441h13.731h1.014h1.161h1.161h0.521h3.624h0.008h1.361h0.953
|
||||
h0.207h1.161h10.929h10.684C86.789,86,88,86,88,86v-1.584c1,0.37,0.918,1.279,0.918,3.223C88.918,89.723,88.283,91,86.375,91z'
|
||||
/>
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeFounder;
|
186
frontend/src/components/Icons/BadgeLimits.tsx
Normal file
186
frontend/src/components/Icons/BadgeLimits.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const BadgeLimits: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} viewBox='0 0 100 100'>
|
||||
<g>
|
||||
<path
|
||||
fill='#FFE47B'
|
||||
d='M100,12.146C100,4.874,94.133,0,86.904,0H13.079C5.852,0,0,4.874,0,12.146v73.666
|
||||
C0,93.084,5.852,100,13.079,100h73.825C94.133,100,100,93.084,100,85.812V12.146z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M42.982,39.041c0.646,0.696,1.7,0.835,2.675,0.447c0.205-0.066,0.396-0.164,0.572-0.293
|
||||
c0.166-0.107,0.327-0.23,0.479-0.371l0.062-0.056c0.089-0.083,0.17-0.169,0.247-0.257l22.319-20.703
|
||||
c0.979-0.871,6.873-6.916,5.815-8.105l-0.055-0.061c-1.057-1.189-8.329,4.508-9.309,5.377L44.932,32.549l-8.202-7.813
|
||||
c-0.891-0.962-4.807-4.321-5.973-3.239l-0.062,0.057c-1.166,1.081,2.524,5.685,3.415,6.646L42.982,39.041z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M28.018,55l0.004-0.051l0.3-1.146c-1.722-0.509-3.59-1.063-5.303-1.544l-4.585,15.679l5.682,1.619
|
||||
L28.021,55H28.018z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M5,47.607v16.729l2.036,0.652l4.629-15.5C9.237,48.781,7,48.07,5,47.607z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M88.421,54.312l-0.216-0.305c-0.309-1.103-1.399-1.873-2.561-1.873c-0.242,0-0.517,0.033-0.751,0.099
|
||||
l-5.704,1.59c-1.391,0.389-2.217,1.816-1.836,3.185l0.262,0.947c-0.757,0.206-1.498,0.431-2.188,0.666l-0.327,0.111l-0.08,0.335
|
||||
c-0.438,1.807-1.39,3.402-2.684,4.49c-0.636,0.536-1.41,0.915-2.3,1.126l-0.054,0.013l-0.051,0.022
|
||||
c-1.358,0.591-3.126,0.56-5.059-0.076c-0.404-0.133-0.792-0.292-1.164-0.469l-0.38,0.514l-0.378,0.511
|
||||
c0.485,0.246,0.998,0.459,1.53,0.635c2.209,0.726,4.263,0.749,5.898,0.057c1.059-0.259,1.989-0.721,2.764-1.374
|
||||
c1.414-1.189,2.472-2.89,3.006-4.816c0.574-0.19,1.181-0.371,1.796-0.539l3.626,12.974c-1.7,0.656-3.707,1.229-5.853,1.725
|
||||
l0.438,0.291c0.087,0.057,0.167,0.12,0.244,0.187c0.181,0.156,0.333,0.336,0.456,0.532c1.826-0.451,3.544-0.964,5.057-1.541
|
||||
c0.357,1.017,1.34,1.715,2.445,1.715c0.241,0,0.483-0.034,0.717-0.1l5.688-1.589c1.391-0.389,2.212-1.818,1.829-3.184l0.102-0.558
|
||||
C93.041,69.504,94,69.395,94,69.285V68.63v-0.657c0,0.147-1.193,0.292-1.643,0.431l-3.471-12.885L94,53.958v-0.657v-0.656
|
||||
L88.421,54.312z M90.994,68.746l0.169,0.604l0.169,0.604l0.146,0.526c0.21,0.753-0.25,1.541-1.025,1.758l-5.688,1.59
|
||||
c-0.133,0.037-0.27,0.057-0.406,0.057c-0.633,0-1.191-0.407-1.372-0.994c-0.004-0.012-0.008-0.022-0.011-0.034l-0.16-0.568
|
||||
l-0.166-0.6L79.473,60.32l-0.404-1.449l-0.169-0.602l-0.168-0.604l-0.104-0.368l-0.168-0.603c-0.211-0.753,0.25-1.542,1.026-1.759
|
||||
l5.687-1.589c0.134-0.038,0.271-0.057,0.405-0.057c0.646,0,1.214,0.423,1.385,1.027l0.094,0.34l0.17,0.604l0.168,0.603
|
||||
L90.994,68.746z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M21.812,51.913l0.004-0.014l0.107-0.299c0.029-0.1,0.077-0.198,0.077-0.298c0-0.004,0-0.008,0-0.013
|
||||
c0-0.224-0.033-0.448-0.111-0.66c-0.024-0.074-0.066-0.146-0.104-0.217c-0.201-0.364-0.537-0.63-0.936-0.746l-6.075-1.776
|
||||
c-0.143-0.041-0.292-0.062-0.438-0.062c-0.576,0-1.101,0.324-1.368,0.819c-0.053,0.095-0.096,0.196-0.127,0.302l-0.086,0.288
|
||||
l-0.176,0.603L8.047,65.342l-0.176,0.601l-0.176,0.602l-0.012,0.038c-0.116,0.4-0.07,0.821,0.129,1.186
|
||||
c0.201,0.367,0.53,0.632,0.931,0.748l6.071,1.775c0.144,0.042,0.291,0.063,0.439,0.063c0.687,0,1.302-0.461,1.495-1.122
|
||||
l0.009-0.034l0.117-0.399l0.06-0.201l0.116-0.4l0.06-0.201l0.116-0.399l4.413-15.096L21.812,51.913z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M61.145,67.641l-1.209,1.634l1.351,0.822c0.037-0.069,0.078-0.139,0.122-0.204l0.986-1.489L61.145,67.641z
|
||||
'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M69.353,79.089l-2.999-1.985c-0.006,0.453-0.14,0.896-0.396,1.281l-1.141,1.722
|
||||
c-0.274,0.416-0.667,0.728-1.116,0.904l3.043,2.015c0.22,0.145,0.473,0.222,0.733,0.222c0.447,0,0.862-0.223,1.108-0.596
|
||||
l1.141-1.721C70.13,80.319,69.963,79.493,69.353,79.089z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M55.339,75.491l-0.376,0.51c0.032,0.031,0.067,0.062,0.103,0.091c0.036-0.002,0.072-0.003,0.108-0.003
|
||||
c0.092,0,0.183,0.006,0.271,0.017c-0.059-0.196-0.091-0.401-0.099-0.606c0-0.006-0.001-0.012-0.001-0.018L55.339,75.491z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M63.935,79.521l1.14-1.723c0.17-0.256,0.244-0.556,0.214-0.858c-0.004-0.046-0.011-0.093-0.02-0.139
|
||||
c-0.035-0.172-0.103-0.332-0.196-0.474c-0.037-0.055-0.079-0.106-0.124-0.155c-0.073-0.08-0.154-0.152-0.248-0.214l-0.277-0.184
|
||||
l-1.188-0.787L62.647,74.6l-3.043-2.016c-0.219-0.145-0.472-0.221-0.732-0.221c-0.302,0-0.586,0.103-0.815,0.282
|
||||
c-0.112,0.087-0.213,0.191-0.293,0.313l-0.115,0.172l-0.346,0.522l-0.68,1.026c-0.162,0.244-0.23,0.523-0.218,0.797
|
||||
c0.021,0.408,0.226,0.803,0.593,1.045l0.03,0.021l0.846,0.559l0.061,0.041l0.782,0.52l0.975,0.645l0.304,0.201l0.562,0.372
|
||||
l0.845,0.56l0.691,0.458c0.219,0.144,0.472,0.221,0.731,0.221C63.273,80.117,63.688,79.895,63.935,79.521z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M75.713,75.141c-0.045-0.039-0.09-0.076-0.141-0.109l-0.566-0.376l-0.726-0.479l-2.267-1.501
|
||||
c0.043,0.53-0.089,1.057-0.388,1.507l-0.383,0.58l-0.476,0.718l-0.28,0.424c-0.073,0.109-0.154,0.212-0.244,0.308
|
||||
c-0.195,0.21-0.424,0.381-0.676,0.508l3.397,2.249c0.219,0.146,0.473,0.222,0.733,0.222c0.447,0,0.862-0.223,1.108-0.596
|
||||
l1.14-1.721C76.317,76.312,76.206,75.571,75.713,75.141z'
|
||||
/>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M62.098,71.475c0.062,0.301,0.22,0.564,0.452,0.756c0.036,0.031,0.076,0.061,0.116,0.088l3.559,2.355
|
||||
l0.621,0.411l0.459,0.305l0.457,0.302c0.146,0.097,0.307,0.162,0.475,0.195c0.033,0.007,0.065,0.012,0.099,0.017
|
||||
c0.053,0.006,0.105,0.01,0.159,0.01c0.228,0,0.447-0.059,0.641-0.165c0.186-0.102,0.348-0.247,0.468-0.43l0.208-0.313l0.933-1.408
|
||||
c0.195-0.296,0.265-0.65,0.194-0.998c-0.071-0.348-0.273-0.648-0.568-0.844l-0.467-0.309l-0.695-0.461l-0.537-0.354l-3.396-2.25
|
||||
c-0.219-0.145-0.472-0.221-0.732-0.221c-0.046,0-0.09,0.004-0.134,0.008c-0.264,0.027-0.51,0.13-0.71,0.296
|
||||
c-0.101,0.083-0.191,0.179-0.266,0.291l-0.133,0.2l-1.007,1.522c-0.037,0.056-0.068,0.114-0.097,0.174
|
||||
c-0.097,0.207-0.137,0.434-0.119,0.662C62.08,71.366,62.086,71.421,62.098,71.475z'
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill='#DD6C01'
|
||||
d='M73.355,57.131c-0.943-4.371-5.124-7.201-8.465-8.806l-0.038-0.019l-12.591-4.036
|
||||
c-0.149-0.071-0.484-0.198-0.97-0.198c-0.864,0-1.964,0.376-3.217,1.648c-0.143,0.145-0.286,0.301-0.433,0.47
|
||||
c-0.128,0.147-0.257,0.304-0.388,0.47c-0.006-0.003-0.01-0.006-0.017-0.009c-0.005,0.006-0.011,0.014-0.016,0.021
|
||||
c-0.298-0.15-0.952-0.402-2.021-0.402c-2.321,0-6.247,1.131-12.321,6.044c3.521-0.469,7.893-0.591,11.463-0.593
|
||||
c-0.141,0.346-0.33,0.811-0.476,1.173l-0.044,0.082c-3.187,0.013-7.911,0.137-11.5,0.684c-0.46,0.069-0.899,0.146-1.315,0.231
|
||||
c-0.021,0.004-0.041,0.008-0.062,0.013c-0.434,0.089-0.839,0.187-1.213,0.293c-0.006,0.002-0.013,0.004-0.021,0.006
|
||||
c-0.075,0.022-0.152,0.043-0.226,0.066l-0.05,0.185l-0.137,0.508l-0.166,0.626l-3.813,14.255l-0.104,0.393
|
||||
c0.054,0.077,0.121,0.173,0.203,0.285c0.155,0.214,0.36,0.491,0.612,0.819c0.401,0.524,0.923,1.181,1.558,1.931l-0.398,0.51
|
||||
l-0.394,0.504l-1.553,1.993c-0.809,1.039-0.623,2.543,0.415,3.354l1.629,1.269c0.424,0.33,0.931,0.505,1.466,0.505
|
||||
c0.401,0,0.786-0.099,1.126-0.28c0.145,0.424,0.409,0.812,0.788,1.106l1.628,1.27c0.425,0.33,0.932,0.504,1.469,0.504
|
||||
c0.74,0,1.428-0.335,1.884-0.919l0.218-0.28l0.321-0.412c0.112,0.428,0.337,0.814,0.658,1.12c0.059,0.055,0.119,0.107,0.182,0.156
|
||||
l1.628,1.27c0.337,0.263,0.727,0.426,1.143,0.481c0.107,0.016,0.215,0.023,0.325,0.023c0.262,0,0.517-0.043,0.759-0.122
|
||||
c0.238-0.079,0.463-0.195,0.665-0.347c0.194,0.105,0.389,0.212,0.586,0.314l1.602-0.501c0.12,0.157,0.259,0.302,0.421,0.429
|
||||
l0.089,0.069l0.602,0.468l0.938,0.731c0.423,0.331,0.931,0.505,1.467,0.505c0.609,0,1.181-0.226,1.618-0.63
|
||||
c0.096-0.089,0.186-0.186,0.267-0.29l0.088-0.112l0.326-0.419l3.291,2.179c0.219,0.146,0.472,0.222,0.731,0.222
|
||||
c0.447,0,0.863-0.223,1.109-0.596l1.141-1.721c0.269-0.406,0.283-0.908,0.084-1.317c-0.102-0.206-0.255-0.389-0.458-0.524
|
||||
l-1.304-0.863l-1.209-0.8l-0.474-0.314l0.239-0.306l0.087-0.113l0.052-0.065l0.054-0.068c0.047-0.062,0.091-0.123,0.131-0.186
|
||||
c0.024-0.038,0.044-0.077,0.066-0.115l2.748,1.819l0.193,0.128c0.271,0.18,0.494,0.406,0.664,0.661l1.696,1.123
|
||||
c0.22,0.145,0.473,0.222,0.732,0.222c0.447,0,0.862-0.223,1.109-0.596l1.14-1.723c0.404-0.61,0.235-1.436-0.374-1.84l-4.387-2.905
|
||||
l-0.709-0.47c-0.054-0.035-0.109-0.066-0.168-0.094c-0.176-0.083-0.368-0.127-0.564-0.127c-0.13,0-0.257,0.021-0.378,0.057
|
||||
c-0.124,0.037-0.24,0.091-0.35,0.161c-0.09,0.06-0.173,0.129-0.247,0.209c-0.048,0.053-0.095,0.107-0.136,0.169l-0.188,0.285
|
||||
l-0.039,0.059l-0.581,0.878c-0.051-0.141-0.113-0.274-0.188-0.402c-0.028-0.047-0.058-0.093-0.089-0.14
|
||||
c-0.092-0.132-0.196-0.256-0.314-0.368c-0.016-0.015-0.032-0.028-0.049-0.043c-0.027-0.025-0.059-0.051-0.088-0.075
|
||||
c-0.016-0.014-0.03-0.028-0.047-0.041l-0.304-0.235l0.287-0.387l0.546-0.739l0.678-0.918l0.617-0.834l0.328-0.444l0.327-0.44
|
||||
l0.619-0.838l0.042-0.057l0.656-0.888l2.046-2.767l0.44-0.597l1.516-2.049l0.439-0.597l1.146-1.547l0.23-0.312
|
||||
c-0.119-0.083-0.236-0.166-0.351-0.249c-0.321-0.229-0.624-0.448-0.973-0.614c-0.515-0.242-1.034-0.52-1.566-0.831
|
||||
c-0.328-0.193-0.661-0.4-1-0.621c-0.184-0.119-0.369-0.243-0.557-0.371c-0.182-0.126-0.365-0.256-0.552-0.389
|
||||
c-0.094-0.067-0.188-0.134-0.281-0.203c-1.568-1.15-3.563-2.688-5.396-4.318c-0.036-0.031-0.071-0.062-0.106-0.094l0.37-0.511
|
||||
l0.117-0.161l0.252-0.349l0.09-0.124l0.333,0.268l0.588,0.473l2.303,1.852c0.186,0.187,1.124,1.108,2.521,2.193
|
||||
c0.089,0.068,0.178,0.138,0.271,0.207c0.181,0.138,0.367,0.276,0.562,0.416c0.304,0.219,0.623,0.441,0.956,0.661
|
||||
c0.205,0.137,0.417,0.271,0.633,0.404c0.785,0.487,1.632,0.813,2.513,1.203c1.501,0.664,3.104,0.981,4.685,0.981c0,0,0,0,0.001,0
|
||||
c2.106,0,3.911-0.646,5.367-2.231l0.062,0.021l0.041-0.049C73.016,61.731,73.934,59.805,73.355,57.131z M41.97,59.117l0.357-0.673
|
||||
l0.388-0.725l1.294-2.43c0.06,0.009,0.12,0.009,0.183-0.002c0.012-0.002,0.467-0.069,1.121,0.024
|
||||
c0.694,0.099,1.614,0.382,2.469,1.122c0.135,0.115,0.267,0.244,0.397,0.384c0.038,0.043,0.077,0.088,0.117,0.133
|
||||
c0.139,0.16,0.274,0.335,0.404,0.526c0.065,0.096,0.128,0.194,0.19,0.297l-0.803,1.105l-0.026,0.041
|
||||
c-1.482,2.415-3.013,3.64-4.549,3.64c-1.254,0-2.243-0.839-2.688-1.297L41.97,59.117z M30.647,78.373l-0.393,0.504
|
||||
c-0.226,0.288-0.371,0.614-0.445,0.949l-0.004,0.005c-0.254,0.325-0.636,0.513-1.05,0.513c-0.297,0-0.578-0.098-0.814-0.281
|
||||
l-1.629-1.269c-0.577-0.451-0.681-1.288-0.23-1.866l1.426-1.829l0.392-0.502l0.398-0.51l1.358-1.744l0.184-0.234
|
||||
c0.086-0.11,0.188-0.204,0.301-0.281c0.217-0.147,0.476-0.23,0.748-0.23c0.299,0,0.581,0.179,0.816,0.362l0.732,0.654h0.001
|
||||
l0.894,0.616c0.281,0.218,0.459,0.49,0.502,0.843c0.011,0.081,0.012,0.144,0.008,0.224l-2.415,3.088l-0.393,0.498L30.647,78.373z
|
||||
M35.794,81.175l-0.389,0.498l-0.382,0.491l-0.209,0.269c-0.255,0.324-0.637,0.511-1.048,0.511c-0.299,0-0.581-0.097-0.817-0.28
|
||||
l-1.628-1.27c-0.357-0.279-0.534-0.708-0.508-1.13c0.016-0.259,0.105-0.516,0.276-0.735l0.331-0.425l0.386-0.493l0.468-0.502
|
||||
L35,74.711v-0.002c1-0.325,0.561-0.513,0.974-0.513c0.298,0,0.542,0.098,0.778,0.281l1.608,1.27
|
||||
c0.279,0.219,0.448,0.533,0.491,0.885c0.044,0.352-0.057,0.7-0.275,0.98L35.794,81.175z M41.175,83.871
|
||||
c-0.188,0.24-0.444,0.403-0.732,0.474c-0.103,0.024-0.209,0.038-0.317,0.038c-0.298,0-0.579-0.098-0.815-0.281l-1.628-1.27
|
||||
c-0.191-0.148-0.334-0.342-0.42-0.562c-0.007-0.005-0.015-0.012-0.023-0.017c-0.035-0.098-0.054-0.2-0.067-0.305
|
||||
c-0.044-0.352,0.058-0.7,0.274-0.98l3.76-4.82c0.254-0.324,0.637-0.512,1.051-0.512c0.298,0,0.581,0.098,0.816,0.281l1.628,1.27
|
||||
c0.578,0.45,0.682,1.287,0.231,1.864L41.175,83.871z M51.46,78.445l0.087,0.068l0.282,0.219c0.07,0.056,0.135,0.117,0.191,0.184
|
||||
c0.002,0.001,0.002,0.003,0.004,0.005c0.014,0.015,0.025,0.032,0.038,0.048c0.114,0.148,0.197,0.318,0.241,0.503
|
||||
c0.002,0.01,0.005,0.02,0.007,0.027c0.005,0.024,0.008,0.048,0.013,0.071c0.002,0.017,0.006,0.031,0.008,0.048
|
||||
c0.025,0.21,0,0.419-0.07,0.613c-0.016,0.044-0.033,0.088-0.055,0.131c-0.006,0.013-0.014,0.026-0.02,0.039
|
||||
c-0.037,0.068-0.079,0.135-0.127,0.196l-0.13,0.166l-0.025,0.033l-0.131,0.167l-0.196,0.252l-0.038,0.049l-0.387,0.498
|
||||
l-0.388,0.497l-1.253,1.605l-0.388,0.498l-0.388,0.497l-0.07,0.091l-0.327,0.419l-0.037,0.048
|
||||
c-0.103,0.131-0.227,0.239-0.365,0.322c-0.203,0.121-0.438,0.189-0.684,0.189c-0.298,0-0.579-0.098-0.814-0.281l-0.361-0.28
|
||||
l-0.601-0.469l-0.601-0.468L44.81,84.38c-0.578-0.45-0.682-1.287-0.23-1.866l3.756-4.818c0.255-0.325,0.638-0.513,1.05-0.513
|
||||
c0.298,0,0.581,0.098,0.816,0.281l0.728,0.566L51.46,78.445z M52.808,50.553c-0.091,0.888-0.312,1.85-0.77,2.768
|
||||
c-0.182,0.248-0.29,0.495-0.439,0.744l-0.08,0.106c0,0.002,0.006,0.003,0.011,0.005c-0.003,0.006-0.007,0.01-0.01,0.015
|
||||
l-0.018-0.012c0.001-0.003,0.002-0.005,0.004-0.008l-0.374,0.516l-0.371,0.512l-0.312,0.428l-0.06,0.083l-0.563,0.777l-0.148,0.203
|
||||
c-0.03-0.043-0.06-0.088-0.091-0.13c-0.143-0.194-0.288-0.374-0.438-0.54c-1.55-1.727-3.449-2.003-4.456-2.011l0.371-0.698
|
||||
l0.026-0.058c0.039-0.093,0.077-0.185,0.114-0.275c0.091-0.214,0.179-0.423,0.268-0.627c0.094-0.213,0.187-0.422,0.279-0.623
|
||||
c0.916-2.004,1.768-3.388,2.523-4.343c0.136-0.173,0.271-0.332,0.4-0.478c0.152-0.169,0.3-0.321,0.44-0.456
|
||||
c0.995-0.951,1.755-1.127,2.176-1.127c0.132,0,0.235,0.019,0.307,0.037c-0.06-0.104-0.095-0.16-0.099-0.166l0.046-0.029
|
||||
c0.003,0.004,0.007,0.007,0.012,0.011l-0.031,0.019c0.003,0.006,0.013,0.062,0.072,0.166c0.015,0.004,0.027,0.007,0.038,0.01
|
||||
c0.096,0.172,0.209,0.46,0.438,0.846c0.232,0.678,0.671,1.648,0.79,2.784L53,49.045c0,0.004,0,0.011,0,0.018l-0.162-0.062
|
||||
c0.027,0.259,0.006,0.524,0.011,0.798c0.002,0.241-0.026,0.486-0.047,0.737L52.808,50.553z M61.043,60.258
|
||||
c-0.156,0.124-0.317,0.246-0.482,0.366c-0.191-0.125-0.378-0.251-0.561-0.378c-0.213-0.147-0.421-0.295-0.622-0.441
|
||||
c-1.861-1.366-3.118-2.632-3.146-2.659l-1.748-1.423l-0.583-0.475l-0.598-0.486l-0.461-0.375c0.557-0.956,0.924-1.977,1.101-3.04
|
||||
c0.039-0.229,0.068-0.461,0.088-0.693c0.022-0.246,0.031-0.493,0.032-0.741c0.005-0.711-0.07-1.434-0.23-2.164
|
||||
c-0.156-0.711-0.371-1.336-0.579-1.84l11.133,3.57c0.061,0.029,0.119,0.059,0.179,0.089c0.04,0.102,0.117,0.312,0.2,0.611
|
||||
c0.184,0.666,0.396,1.773,0.295,3.104c-0.154,2.033-0.965,3.868-2.419,5.485c-0.309,0.344-0.646,0.677-1.012,1
|
||||
C61.441,59.934,61.245,60.098,61.043,60.258z M71.878,61.25c-0.609,0.648-1.276,1.133-2.01,1.455
|
||||
c-0.437,0.191-0.896,0.399-1.382,0.476c-0.315,0.049-0.643,0.148-0.98,0.148h-0.001c-1.953,0-3.997-0.893-5.798-1.927
|
||||
c0.19-0.147,0.372-0.335,0.549-0.485c0.161-0.139,0.318-0.297,0.469-0.437c0.158-0.146,0.307-0.305,0.453-0.453
|
||||
c0.023-0.024,0.049-0.054,0.073-0.08c0.233-0.243,0.448-0.488,0.649-0.732c0.193-0.237,0.375-0.477,0.542-0.714
|
||||
c1.306-1.862,1.759-3.693,1.867-5.123c0.091-1.188-0.039-2.222-0.203-2.986c3.353,1.955,5.418,4.335,6.008,6.943
|
||||
C72.567,59.333,72.021,60.896,71.878,61.25z'
|
||||
/>
|
||||
</g>
|
||||
<circle fill='#DD6C01' cx='81.483' cy='57.609' r='1.819' />
|
||||
<circle fill='#DD6C01' cx='85.483' cy='70.609' r='1.819' />
|
||||
<circle fill='#FFE47B' cx='23.483' cy='60.609' r='1.819' />
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeLimits;
|
98
frontend/src/components/Icons/BadgeLoved.tsx
Normal file
98
frontend/src/components/Icons/BadgeLoved.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const BadgeLoved: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} viewBox='0 0 100 100'>
|
||||
<path
|
||||
fill='#FF9FB3'
|
||||
d='M100,12.785C100,5.444,93.838,0,86.466,0H13.645C6.271,0,0,5.444,0,12.785v72.938
|
||||
C0,93.064,6.271,100,13.645,100h72.821C93.838,100,100,93.064,100,85.723V12.785z'
|
||||
/>
|
||||
<g>
|
||||
<path fill='#CF2B00' d='M17,30.843v-0.246C17,30.643,16.997,30.737,17,30.843z' />
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M83.416,16H59.137H52.83h-1.967C50.387,16,50,16.387,50,16.863v0.273C50,17.613,50.387,18,50.863,18H54
|
||||
h5.137h24.279C85.821,18,89,18.849,89,21.314v33.497C89,57.294,85.782,60,83.416,60h-7.754l-5.83,4.802L71.482,60H22.285
|
||||
C19.841,60,19,57.172,19,54.812V33v-1v-1c0-0.553-0.447-1-1-1c-0.538,0-0.968,0.426-0.991,0.958
|
||||
c-0.005-0.04-0.008-0.077-0.009-0.115V31v2v21.812C17,58.559,18.452,62,22.285,62H68.06l-3.349,9.739L76.538,62h6.878
|
||||
C87.113,62,91,58.623,91,54.812V21.314C91,17.496,87.175,16,83.416,16z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M80.554,33H26.932c-0.451,0-0.814,0.551-0.814,1s0.363,1,0.814,1h53.622c0.449,0,0.814-0.551,0.814-1
|
||||
S81.003,33,80.554,33z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M81.368,42c0-0.449-0.365-1-0.814-1H26.932c-0.451,0-0.814,0.551-0.814,1s0.363,1,0.814,1h53.622
|
||||
C81.003,43,81.368,42.449,81.368,42z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M26.683,49c-0.284,0-0.512,0.545-0.512,1s0.228,1,0.512,1h33.216c0.282,0,0.511-0.545,0.511-1
|
||||
s-0.229-1-0.511-1H26.683z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M31.007,16.333C31.022,16.35,31.015,16.314,31.007,16.333L31.007,16.333z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M18,24.707c0,0.415,0,0.83,0,1.245c0,0.38,0.078,0.595,0.234,0.767c0.219,0.24,0.234,0.188,0.426,0.281
|
||||
h10.241c0.709,0,2.11-0.004,2.099-1.048v-8.894v-0.649c0-0.046,0.003-0.067,0.007-0.076c-0.006-0.006-0.015-0.017-0.03-0.049
|
||||
C30.764,15.858,29.422,16,28.901,16h-8.143c0.01,0,0.021-0.687,0.03-0.824c0.115-1.422,0.311-2.85,0.8-4.197
|
||||
c0.591-1.627,2.094-2.839,3.578-3.627c0.769-0.408,1.863-0.609,2.161-1.551c0.438-1.384-1.191-1.764-2.201-1.461
|
||||
c-1.516,0.453-3.095,1.1-4.206,2.262c-1.041,1.09-1.793,2.518-2.247,3.946c-0.28,0.881-0.453,1.798-0.551,2.717
|
||||
c-0.119,1.119-0.149,2.247-0.15,3.376c-0.002,1.063,0.023,2.127,0.024,3.186C17.999,21.453,18.003,23.08,18,24.707z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M36.129,76.546c-0.83-0.203-3.49-0.496-5.189-0.659c-0.685-1.565-1.717-3.837-2.148-4.533
|
||||
c-0.274-0.44-0.654-0.98-1.23-0.965c-0.576-0.016-0.958,0.524-1.231,0.965c-0.43,0.696-1.465,2.968-2.148,4.533
|
||||
c-1.698,0.163-4.359,0.456-5.188,0.659c-0.506,0.122-1.137,0.319-1.301,0.869c-0.191,0.542,0.203,1.073,0.538,1.47
|
||||
c0.551,0.651,2.526,2.457,3.804,3.591c-0.465,1.645-1.145,4.195-1.256,5.035c-0.067,0.516-0.115,1.174,0.341,1.526
|
||||
c0.434,0.375,1.071,0.199,1.563,0.031c0.835-0.286,3.373-1.547,4.879-2.323c1.504,0.776,4.045,2.037,4.879,2.323
|
||||
c0.492,0.168,1.129,0.344,1.564-0.031c0.454-0.353,0.409-1.011,0.341-1.526c-0.112-0.84-0.793-3.391-1.258-5.035
|
||||
c1.277-1.134,3.253-2.939,3.806-3.591c0.335-0.396,0.728-0.928,0.535-1.47C37.264,76.865,36.635,76.668,36.129,76.546z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M61.442,76.546c-0.828-0.203-3.49-0.496-5.188-0.659c-0.685-1.565-1.718-3.837-2.149-4.533
|
||||
c-0.272-0.44-0.653-0.98-1.229-0.965c-0.575-0.016-0.957,0.524-1.23,0.965c-0.432,0.696-1.465,2.968-2.149,4.533
|
||||
c-1.699,0.163-4.358,0.456-5.188,0.659c-0.505,0.122-1.137,0.319-1.3,0.869c-0.192,0.542,0.201,1.073,0.537,1.47
|
||||
c0.551,0.651,2.525,2.457,3.805,3.591c-0.466,1.645-1.146,4.195-1.256,5.035c-0.068,0.516-0.115,1.174,0.34,1.526
|
||||
c0.435,0.375,1.07,0.199,1.564,0.031c0.834-0.286,3.372-1.547,4.878-2.323c1.505,0.776,4.045,2.037,4.879,2.323
|
||||
c0.491,0.168,1.129,0.344,1.564-0.031c0.454-0.353,0.409-1.011,0.34-1.526c-0.111-0.84-0.791-3.391-1.258-5.035
|
||||
c1.278-1.134,3.254-2.939,3.806-3.591c0.336-0.396,0.729-0.928,0.537-1.47C62.579,76.865,61.949,76.668,61.442,76.546z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M86.758,76.546c-0.83-0.203-3.49-0.496-5.189-0.659c-0.685-1.565-1.718-3.837-2.148-4.533
|
||||
c-0.273-0.44-0.654-0.98-1.23-0.965c-0.574-0.016-0.958,0.524-1.229,0.965c-0.433,0.696-1.465,2.968-2.149,4.533
|
||||
c-1.699,0.163-4.359,0.456-5.188,0.659c-0.507,0.122-1.139,0.319-1.301,0.869c-0.191,0.542,0.202,1.073,0.538,1.47
|
||||
c0.55,0.651,2.525,2.457,3.804,3.591c-0.466,1.645-1.145,4.195-1.255,5.035c-0.069,0.516-0.116,1.174,0.339,1.526
|
||||
c0.435,0.375,1.071,0.199,1.563,0.031c0.835-0.286,3.374-1.547,4.879-2.323c1.505,0.776,4.045,2.037,4.879,2.323
|
||||
c0.492,0.168,1.129,0.345,1.565-0.031c0.453-0.353,0.409-1.011,0.34-1.526c-0.111-0.84-0.792-3.391-1.258-5.035
|
||||
c1.278-1.134,3.253-2.939,3.806-3.591c0.335-0.396,0.729-0.928,0.535-1.469C87.894,76.865,87.263,76.668,86.758,76.546z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M34,24.707c0,0.415,0,0.83,0,1.245c0,0.38,0.078,0.595,0.234,0.767c0.219,0.24,0.234,0.188,0.426,0.281
|
||||
h10.241c0.709,0,2.11-0.004,2.099-1.048v-8.894v-0.649c0-0.046,0.003-0.067,0.007-0.076c-0.006-0.006-0.015-0.017-0.03-0.049
|
||||
C46.764,15.858,45.422,16,44.901,16h-8.143c0.01,0,0.021-0.687,0.03-0.824c0.115-1.422,0.311-2.85,0.8-4.197
|
||||
c0.591-1.627,2.094-2.839,3.578-3.627c0.769-0.408,1.863-0.609,2.161-1.551c0.438-1.384-1.191-1.764-2.201-1.461
|
||||
c-1.516,0.453-3.095,1.1-4.206,2.262c-1.041,1.09-1.793,2.518-2.247,3.946c-0.28,0.881-0.453,1.798-0.551,2.717
|
||||
c-0.119,1.119-0.149,2.247-0.15,3.376c-0.002,1.063,0.023,2.127,0.024,3.186C33.999,21.453,34.003,23.08,34,24.707z'
|
||||
/>
|
||||
<path
|
||||
fill='#CF2B00'
|
||||
d='M47.007,16.333C47.022,16.35,47.015,16.314,47.007,16.333L47.007,16.333z'
|
||||
/>
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeLoved;
|
278
frontend/src/components/Icons/BadgePrivacy.tsx
Normal file
278
frontend/src/components/Icons/BadgePrivacy.tsx
Normal file
@ -0,0 +1,278 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const BadgePrivacy: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} viewBox='0 0 100 100'>
|
||||
<g>
|
||||
<path
|
||||
fill='#91EA9A'
|
||||
d='M100,13.478C100,6.049,93.421,0,85.938,0H12.428C4.946,0,0,6.049,0,13.478v73.705
|
||||
C0,94.611,4.946,101,12.428,101h73.511C93.421,101,100,94.611,100,87.183V13.478z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='none'
|
||||
d='M50.406,53.726C47.937,53.604,45.774,55,45.007,58h0.562h9.139h0.49C54.551,55,52.688,53.848,50.406,53.726z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M64.986,21.303c-1.371-1.979-2.342-3.919-3.026-5.637l-15.166,0.258l-8.718-0.207
|
||||
c-0.685,1.707-1.652,3.635-3.013,5.597c-3.098,4.467-8.825,9.942-19.044,12.47c-0.262,4.29-0.197,11.291,1.855,19.046
|
||||
c1.092,4.13,2.607,7.956,4.539,11.473c0.13-0.057,0.28-0.07,0.425-0.021c0.367,0.122,2.048,0.173,2.776,0.106
|
||||
c0.045-0.954,0.102-3.551,0.141-5.713c-0.653-0.275-1.112-0.923-1.112-1.676c0-1.001,0.813-1.815,1.816-1.815
|
||||
c1.002,0,1.816,0.814,1.816,1.815c0,0.836-0.567,1.539-1.336,1.752c-0.104,5.926-0.172,6.169-0.195,6.252
|
||||
c-0.048,0.171-0.167,0.314-0.334,0.404c-0.262,0.138-0.942,0.192-1.672,0.192c-0.574,0-0.769-0.033-1.226-0.083
|
||||
C25.943,69.641,29,73.322,33,76.551v-4.978l-2.953-0.19l-0.141-4.706c-0.695-0.256-1.295-0.922-1.295-1.706
|
||||
c0-1.001,0.763-1.815,1.765-1.815s1.792,0.814,1.792,1.815c0,0.811-0.545,1.498-1.277,1.732l0.266,3.595L34,70.483v7.083
|
||||
c1,0.481,1,0.95,2,1.41V62.271c-1-0.288-1.494-0.918-1.494-1.648c0-1.001,0.596-1.814,1.598-1.814c1,0,2.057,0.813,2.057,1.814
|
||||
c0,0.86-0.16,1.581-1.16,1.769v17.456c4,2.877,8.383,5.308,13.529,7.283C55.652,85.165,61,82.748,64,79.89V62.32
|
||||
c0-0.26-1.328-0.922-1.328-1.698c0-1.001,0.738-1.814,1.74-1.814s1.844,0.813,1.844,1.814c0,0.817-0.256,1.51-1.256,1.738v16.66
|
||||
c1-0.425,1-0.861,2-1.307v-7.231l2.383-0.191l-0.113-3.664c-0.631-0.285-1.133-0.919-1.133-1.656c0-1.001,0.785-1.815,1.786-1.815
|
||||
c1.003,0,1.802,0.814,1.802,1.815c0,0.854-0.599,1.571-1.394,1.766l0.142,4.652L68,71.576v5.132c4-3.262,6.622-6.99,9.09-11.176
|
||||
c-0.429,0.04-1.029,0.066-1.533,0.066c-0.73,0-1.448-0.055-1.71-0.192c-0.167-0.09-0.307-0.233-0.355-0.405
|
||||
c-0.022-0.082-0.098-0.329-0.203-6.251c-0.77-0.213-1.341-0.916-1.341-1.752c0-1.001,0.813-1.815,1.814-1.815
|
||||
c1.003,0,1.815,0.814,1.815,1.815c0,0.753-0.46,1.4-1.114,1.676c0.039,2.162,0.096,4.758,0.14,5.709
|
||||
c0.729,0.067,2.41,0.019,2.776-0.104c0.095-0.032,0.189-0.035,0.28-0.021c1.85-3.374,3.317-7.032,4.395-10.971
|
||||
c2.152-7.86,2.224-15.104,1.964-19.505C73.806,31.252,68.081,25.771,64.986,21.303z M41.152,26.227
|
||||
c0.148-0.506,0.729-0.686,1.191-0.799c0.76-0.185,3.198-0.454,4.754-0.602c0.628-1.437,1.573-3.518,1.97-4.154
|
||||
c0.25-0.404,0.601-0.899,1.127-0.885c0.527-0.015,0.877,0.48,1.127,0.885c0.396,0.637,1.342,2.718,1.97,4.154
|
||||
c1.556,0.147,3.995,0.417,4.754,0.602c0.464,0.113,1.041,0.293,1.191,0.799c0.132,0.372-0.039,0.738-0.262,1.053
|
||||
c-0.074,0.104-0.153,0.203-0.23,0.293c-0.506,0.596-2.314,2.251-3.486,3.289c0.1,0.353,0.21,0.75,0.322,1.162
|
||||
c0.368,1.353,0.753,2.862,0.831,3.452c0.045,0.342,0.079,0.754-0.064,1.077c-0.056,0.123-0.134,0.232-0.248,0.32
|
||||
c-0.399,0.346-0.983,0.184-1.434,0.029c-0.764-0.262-3.092-1.416-4.471-2.129c-1.379,0.713-3.705,1.867-4.47,2.129
|
||||
c-0.451,0.154-1.035,0.316-1.434-0.029c-0.114-0.088-0.193-0.197-0.247-0.32c-0.145-0.323-0.109-0.735-0.063-1.077
|
||||
c0.077-0.59,0.461-2.1,0.828-3.452c0.112-0.412,0.223-0.81,0.322-1.162c-1.172-1.038-2.98-2.693-3.485-3.289
|
||||
c-0.077-0.09-0.156-0.189-0.23-0.293C41.19,26.965,41.02,26.598,41.152,26.227z M38.479,48.123
|
||||
c-0.054,0.123-0.133,0.232-0.247,0.322c-0.399,0.345-0.983,0.184-1.433,0.028c-0.766-0.261-3.093-1.417-4.473-2.128
|
||||
c-1.379,0.711-3.704,1.867-4.47,2.128c-0.451,0.155-1.033,0.316-1.433-0.028c-0.114-0.09-0.193-0.199-0.248-0.322
|
||||
c-0.143-0.323-0.109-0.734-0.064-1.077c0.078-0.591,0.463-2.099,0.829-3.45c0.112-0.412,0.223-0.811,0.322-1.163
|
||||
c-1.171-1.037-2.98-2.692-3.484-3.29c-0.077-0.091-0.157-0.189-0.231-0.293c-0.223-0.313-0.394-0.68-0.263-1.053
|
||||
c0.15-0.505,0.729-0.686,1.192-0.798c0.759-0.185,3.196-0.454,4.755-0.604c0.627-1.434,1.574-3.514,1.968-4.15
|
||||
c0.252-0.406,0.601-0.9,1.127-0.887c0.528-0.014,0.877,0.48,1.127,0.887c0.396,0.637,1.342,2.717,1.97,4.15
|
||||
c1.556,0.15,3.996,0.42,4.754,0.604c0.464,0.112,1.042,0.293,1.192,0.798c0.133,0.373-0.038,0.739-0.263,1.053
|
||||
c-0.073,0.104-0.153,0.202-0.229,0.293c-0.505,0.598-2.316,2.253-3.487,3.29c0.1,0.353,0.21,0.751,0.322,1.163
|
||||
c0.368,1.352,0.752,2.859,0.83,3.45C38.589,47.389,38.623,47.8,38.479,48.123z M61,59.447v14.64C61,74.849,59.623,76,58.862,76
|
||||
H41.19C40.429,76,40,74.849,40,74.087v-14.64v-1.208c0-0.639,0.344-1.192,0.934-1.348c-0.002,0.003-0.155,0.01-0.155,0.015
|
||||
c0.072-0.042,0.055,0.586,0.142,0.553C40.925,57.416,40.834,58,40.84,58c0.003,0,0.004,0,0.007,0c0.903-5,4.992-9.174,9.772-8.915
|
||||
c4.448,0.235,7.996,3.735,8.729,8.325c0.006,0.035,0.007-0.094,0.013-0.06c0.07,0.042,0.128,0.004,0.189,0.054
|
||||
c-0.013-0.123,0.202-0.286,0.181-0.409C60.251,57.192,61,57.651,61,58.239V59.447z M76.504,38.85
|
||||
c-0.073,0.104-0.153,0.202-0.23,0.293c-0.504,0.598-2.314,2.252-3.485,3.29c0.1,0.353,0.21,0.751,0.322,1.163
|
||||
c0.367,1.352,0.752,2.859,0.83,3.45c0.045,0.343,0.079,0.754-0.064,1.076c-0.056,0.124-0.134,0.233-0.248,0.323
|
||||
c-0.397,0.345-0.982,0.184-1.433,0.028c-0.765-0.263-3.092-1.417-4.471-2.128c-1.378,0.711-3.705,1.865-4.47,2.128
|
||||
c-0.451,0.155-1.035,0.316-1.434-0.028c-0.114-0.09-0.192-0.199-0.247-0.32c-0.145-0.324-0.11-0.736-0.063-1.079
|
||||
c0.077-0.591,0.461-2.099,0.828-3.45c0.111-0.412,0.222-0.811,0.321-1.163c-1.171-1.038-2.979-2.692-3.485-3.29
|
||||
c-0.077-0.091-0.156-0.188-0.23-0.293c-0.224-0.313-0.394-0.68-0.262-1.053c0.15-0.506,0.728-0.686,1.191-0.798
|
||||
c0.76-0.185,3.197-0.456,4.754-0.603c0.628-1.436,1.574-3.516,1.97-4.152c0.251-0.406,0.601-0.9,1.127-0.887
|
||||
c0.526-0.014,0.877,0.48,1.127,0.887c0.396,0.637,1.342,2.717,1.97,4.152c1.556,0.146,3.994,0.418,4.754,0.603
|
||||
c0.462,0.112,1.041,0.292,1.191,0.798C76.899,38.17,76.729,38.536,76.504,38.85z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M50.579,50.516c-0.063-0.002-0.137-0.349-0.21-0.364c-0.072-0.017-0.146,0.461-0.21,0.461
|
||||
c-3.841,0-7.092,2.388-7.936,7.388h0.36h1.018c0.825-4,3.618-6.283,6.876-6.121c2.933,0.156,6.007,1.992,6.761,5.258
|
||||
C57.262,57.135,58,56.795,58,56.792c0-0.001,0-0.018,0-0.018l-0.689,0.673c-0.01,0-0.709,0.553-0.717,0.553h1.048h0.229
|
||||
C57,53,54.144,50.705,50.579,50.516z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M65.187,60.622c0-0.468-0.381-0.849-0.851-0.849c-0.469,0-0.851,0.381-0.851,0.849
|
||||
c0,0.246,0.105,0.467,0.272,0.622c0.151,0.143,0.354,0.229,0.578,0.229c0.186,0,0.357-0.061,0.497-0.161
|
||||
C65.047,61.157,65.187,60.905,65.187,60.622z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M36.733,60.622c0-0.468-0.382-0.849-0.852-0.849c-0.469,0-0.85,0.381-0.85,0.849
|
||||
c0,0.208,0.075,0.397,0.198,0.546c0.156,0.187,0.391,0.306,0.651,0.306c0.145,0,0.279-0.036,0.398-0.099
|
||||
C36.549,61.231,36.733,60.948,36.733,60.622z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M52.479,61.55c-0.219-0.181-0.625-0.333-0.886-0.453c-0.419-0.189-0.346-0.297-0.834-0.297
|
||||
c-1.814,0-2.76,1.479-2.76,3.291c0,0.001,0,0.001,0,0.003c0,0.202-0.585,0.398-0.55,0.592c-0.035,0.192-0.351,0.392-0.351,0.595
|
||||
c0-0.203-0.135-0.517-0.1-0.709C47.215,65.76,47.933,67,49.052,67c0.001,0-0.009,0-0.008,0c-0.08,1-0.171,0.815-0.262,1.183
|
||||
c-0.2,0.823-0.419,1.73-0.601,2.411c-0.237,0.896-0.254,1.277-0.254,1.277c0.023,0.117,0.079,0.501,0.152,0.573
|
||||
C48.307,72.67,48.69,73,48.69,73h0.761h0.617h0.612h0.751c0,0,0.435-0.418,0.596-0.594l0,0c0.007-0.01,0.015-0.156,0.021-0.164
|
||||
c0.012-0.017,0.429-0.101,0.438-0.118C52.555,72.022,53,71.841,53,71.688c0-0.01,0-0.02,0-0.03c0-0.159-0.447-0.528-0.525-0.825
|
||||
C52.281,70.111,51.711,70,51.47,68c0.001,0-0.046,0-0.046,0c-0.096-1-0.213-0.604-0.298-0.965c0-0.004,0.339,0.077,0.338,0.072
|
||||
C52.584,66.74,53,65.86,54,64.687C54,64.683,54,65,54,65h-0.638C53.398,65,54,64.286,54,64.079c0-0.001,0-0.001,0-0.002
|
||||
C54,63.06,53.208,62.15,52.479,61.55z M50.058,60.726c0.001,0,0.001,0,0.002,0c0.351,0,0.687,0.058,1.004,0.16
|
||||
C50.745,60.783,50.406,60.726,50.058,60.726z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M73.759,56.149c-0.469,0-0.85,0.381-0.85,0.849c0,0.282,0.139,0.533,0.351,0.688
|
||||
c0.142,0.102,0.312,0.163,0.499,0.163c0.28,0,0.529-0.137,0.684-0.347c0.104-0.142,0.167-0.315,0.167-0.505
|
||||
C74.609,56.53,74.229,56.149,73.759,56.149z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M31.176,64.971c0-0.468-0.381-0.849-0.851-0.849c-0.469,0-0.85,0.381-0.85,0.849
|
||||
c0,0.231,0.092,0.442,0.241,0.595c0.155,0.158,0.37,0.258,0.608,0.258c0.218,0,0.416-0.084,0.567-0.219
|
||||
C31.066,65.448,31.176,65.223,31.176,64.971z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M26.958,57.687c0.213-0.155,0.352-0.406,0.352-0.688c0-0.468-0.381-0.849-0.85-0.849
|
||||
c-0.47,0-0.851,0.381-0.851,0.849c0,0.189,0.062,0.363,0.167,0.505c0.154,0.21,0.403,0.347,0.684,0.347
|
||||
C26.646,57.85,26.817,57.788,26.958,57.687z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M69.205,18.381c-2.209-3.189-3.136-6.219-3.511-7.91L46.843,10.79l-12.504-0.294
|
||||
c-0.964,4.14-5.284,16.263-23.051,19.004c-0.433,3.211-1.389,13.333,1.653,24.751c4.889,18.347,17.261,31.211,36.775,38.238
|
||||
l0.287,0.072l0.348-0.078c19.505-7.027,31.871-19.891,36.759-38.23c3.041-11.418,2.085-21.54,1.654-24.75
|
||||
C77.828,27.814,72.103,22.562,69.205,18.381z M83.506,53.687c-4.507,16.467-15.59,28.19-32.942,34.848l-0.538,0.207l-0.54-0.207
|
||||
c-17.522-6.723-28.647-18.605-33.066-35.32c-2.077-7.854-2.182-14.95-1.902-19.521l0.067-1.104l1.074-0.265
|
||||
c9.915-2.453,15.352-7.808,18.169-11.867c1.144-1.651,2.105-3.435,2.853-5.3l0.389-0.969l9.761,0.23l16.144-0.275l0.385,0.965
|
||||
c0.749,1.877,1.713,3.674,2.866,5.339c2.812,4.062,8.247,9.419,18.156,11.877l1.075,0.265l0.065,1.105
|
||||
C85.801,38.434,85.68,45.75,83.506,53.687z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M70.324,65.703c0.252-0.147,0.419-0.42,0.419-0.732c0-0.468-0.381-0.849-0.85-0.849
|
||||
c-0.47,0-0.851,0.381-0.851,0.849c0,0.15,0.039,0.291,0.106,0.413c0.146,0.262,0.424,0.439,0.744,0.439
|
||||
C70.052,65.823,70.198,65.779,70.324,65.703z'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill='none'
|
||||
d='M50.406,53.726C47.937,53.604,45.774,55,45.007,58h0.562h9.139h0.49C54.551,55,52.688,53.848,50.406,53.726z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M47.445,66.015c0.214,0.31,0.471,0.573,0.767,0.748C47.917,66.556,47.663,66.3,47.445,66.015z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M49.017,67.176c-0.075,0.794-0.155,0.699-0.239,1.042c-0.201,0.823-0.417,1.821-0.598,2.503
|
||||
c-0.239,0.894-0.254,1.32-0.254,1.32c0.023,0.116,0.08,0.346,0.153,0.418C48.306,72.684,48.69,73,48.69,73h0.761h0.617h0.612h0.751
|
||||
c0,0,0.536-0.418,0.697-0.594c0.007-0.01,0.115-0.297,0.121-0.304c0.012-0.017,0.227-0.032,0.236-0.048
|
||||
c0.068-0.104-1.303,0.041-1.303-0.112c0-0.011,0.463-0.101,0.452-0.101c-1.239,0.009-0.07-0.629-0.15-0.928
|
||||
C51.292,70.195,51.616,70,51.376,68c0.001,0,0.003,0,0.003,0c-0.079,0-0.145-0.401-0.212-0.746
|
||||
c-0.304,0.086-0.618,0.146-0.949,0.146C49.794,67.4,49.393,67.313,49.017,67.176z'
|
||||
/>
|
||||
<path
|
||||
fill='none'
|
||||
d='M50.579,50.023c-0.063-0.003-0.347,0.027-0.412,0.027c-3.841,0-7.1,2.949-7.944,7.949h0.36h1.018
|
||||
c0.825-4,3.618-6.264,6.876-6.101c2.884,0.153,5.29,1.955,6.084,5.131c0.427-0.009,0.989-0.097,1.454-0.062
|
||||
C57.144,53.209,54.143,50.213,50.579,50.023z'
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M91.562,27.889l-0.202-1.074l-1.086-0.139C70.261,24.119,68.516,9.418,68.452,8.799l-0.134-1.385
|
||||
L46.854,7.779L31.735,7.423l-0.135,1.393c-0.004,0.038-0.395,3.771-3.229,7.858c-3.848,5.551-10.104,8.916-18.595,9.998
|
||||
l-1.085,0.139l-0.204,1.076c-0.095,0.507-2.302,12.593,1.514,27.023C13.536,68.275,23.13,86.142,48.77,95.348l1.197,0.309
|
||||
l1.139-0.258l0.176-0.051c25.64-9.206,35.233-27.071,38.77-40.437C93.864,40.482,91.659,28.396,91.562,27.889z M87.11,54.253
|
||||
c-4.888,18.34-17.254,31.203-36.759,38.23l-0.348,0.078l-0.287-0.072C30.202,85.462,17.83,72.598,12.941,54.251
|
||||
c-3.042-11.418-2.086-21.54-1.653-24.751c17.767-2.741,22.087-14.864,23.051-19.004l12.504,0.294l18.852-0.319
|
||||
c0.375,1.691,1.302,4.721,3.511,7.91c2.897,4.182,8.623,9.434,19.56,11.122C89.195,32.713,90.151,42.835,87.11,54.253z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M85.455,32.588l-1.075-0.265c-9.909-2.458-15.344-7.814-18.156-11.877
|
||||
c-1.153-1.665-2.117-3.462-2.866-5.339l-0.385-0.965l-16.144,0.275l-9.761-0.23l-0.389,0.969c-0.747,1.865-1.709,3.648-2.853,5.3
|
||||
c-2.817,4.06-8.254,9.414-18.169,11.867l-1.074,0.265l-0.067,1.104c-0.279,4.571-0.175,11.668,1.902,19.521
|
||||
c4.419,16.715,15.544,28.598,33.066,35.32l0.54,0.207l0.538-0.207c17.353-6.657,28.436-18.381,32.942-34.848
|
||||
c2.174-7.937,2.295-15.253,2.015-19.993L85.455,32.588z M36.28,61.375c-0.119,0.062-0.254,0.099-0.398,0.099
|
||||
c-0.261,0-0.495-0.119-0.651-0.306c-0.123-0.148-0.198-0.338-0.198-0.546c0-0.468,0.381-0.849,0.85-0.849
|
||||
c0.47,0,0.852,0.381,0.852,0.849C36.733,60.948,36.549,61.231,36.28,61.375z M64.336,61.474c-0.224,0-0.427-0.087-0.578-0.229
|
||||
c-0.167-0.155-0.272-0.376-0.272-0.622c0-0.468,0.382-0.849,0.851-0.849c0.47,0,0.851,0.381,0.851,0.849
|
||||
c0,0.283-0.14,0.535-0.354,0.69C64.693,61.413,64.521,61.474,64.336,61.474z M82.054,53.288
|
||||
c-1.077,3.938-2.545,7.597-4.395,10.971c-0.091-0.015-0.186-0.012-0.28,0.021c-0.366,0.122-2.047,0.171-2.776,0.104
|
||||
c-0.044-0.951-0.101-3.547-0.14-5.709c0.654-0.275,1.113-0.923,1.113-1.676c0-1.001-0.815-1.815-1.817-1.815
|
||||
s-1.817,0.814-1.817,1.815c0,0.836,0.568,1.539,1.337,1.752c0.105,5.922,0.173,6.169,0.195,6.251
|
||||
c0.048,0.172,0.167,0.315,0.334,0.405c0.262,0.138,0.941,0.192,1.672,0.192c0.502,0,1.182-0.026,1.61-0.066
|
||||
C74.622,69.718,72,73.446,68,76.708v-5.132l2.468-0.188L70.4,66.736c0.795-0.194,1.35-0.911,1.35-1.766
|
||||
c0-1.001-0.836-1.815-1.838-1.815s-1.826,0.814-1.826,1.815c0,0.737,0.436,1.371,1.066,1.656l0.232,3.664L67,70.482v7.231
|
||||
c-1,0.445-1,0.882-2,1.307V62.36c1-0.229,1.219-0.921,1.219-1.738c0-1.001-0.849-1.814-1.852-1.814
|
||||
c-1.002,0-1.68,0.813-1.68,1.814c0,0.776,1.312,1.438,1.312,1.698V79.89c-3,2.858-8.348,5.275-13.471,7.24
|
||||
C45.383,85.154,41,82.724,37,79.847V62.391c1-0.188,1.051-0.908,1.051-1.769c0-1.001-0.991-1.814-1.993-1.814
|
||||
c-1,0-1.465,0.813-1.465,1.814c0,0.73,0.407,1.36,1.407,1.648v16.706c-1-0.46-1-0.929-2-1.41v-7.083l-2.851-0.186l-0.111-3.595
|
||||
c0.732-0.234,1.184-0.922,1.184-1.732c0-1.001-0.854-1.815-1.856-1.815s-1.837,0.814-1.837,1.815c0,0.784,0.489,1.45,1.185,1.706
|
||||
l0.339,4.706L33,71.573v4.978c-4-3.229-7.057-6.91-9.486-11.035c0.457,0.05,0.855,0.083,1.43,0.083c0.73,0,1.31-0.055,1.57-0.192
|
||||
c0.168-0.09,0.234-0.233,0.281-0.404c0.024-0.083,0.066-0.326,0.171-6.252c0.769-0.213,1.324-0.916,1.324-1.752
|
||||
c0-1.001-0.822-1.815-1.824-1.815s-1.819,0.814-1.819,1.815c0,0.753,0.457,1.4,1.11,1.676c-0.038,2.162-0.097,4.759-0.142,5.713
|
||||
c-0.729,0.066-2.409,0.016-2.776-0.106c-0.145-0.049-0.295-0.035-0.425,0.021c-1.932-3.517-3.447-7.343-4.539-11.473
|
||||
c-2.053-7.755-2.117-14.756-1.855-19.046c10.219-2.527,15.946-8.003,19.044-12.47c1.36-1.962,2.328-3.89,3.013-5.597l8.718,0.207
|
||||
l15.166-0.258c0.685,1.718,1.655,3.658,3.026,5.637c3.095,4.469,8.819,9.949,19.031,12.48
|
||||
C84.277,38.185,84.206,45.428,82.054,53.288z M74.442,57.503c-0.154,0.21-0.403,0.347-0.684,0.347
|
||||
c-0.187,0-0.357-0.062-0.499-0.163c-0.212-0.155-0.351-0.406-0.351-0.688c0-0.468,0.381-0.849,0.85-0.849
|
||||
c0.47,0,0.851,0.381,0.851,0.849C74.609,57.188,74.547,57.361,74.442,57.503z M69.894,65.823c-0.32,0-0.598-0.178-0.744-0.439
|
||||
c-0.067-0.122-0.106-0.263-0.106-0.413c0-0.468,0.381-0.849,0.851-0.849c0.469,0,0.85,0.381,0.85,0.849
|
||||
c0,0.312-0.167,0.585-0.419,0.732C70.198,65.779,70.052,65.823,69.894,65.823z M30.893,65.604
|
||||
c-0.151,0.135-0.35,0.219-0.567,0.219c-0.238,0-0.453-0.1-0.608-0.258c-0.149-0.152-0.241-0.363-0.241-0.595
|
||||
c0-0.468,0.381-0.849,0.85-0.849c0.47,0,0.851,0.381,0.851,0.849C31.176,65.223,31.066,65.448,30.893,65.604z M25.776,57.503
|
||||
c-0.104-0.142-0.167-0.315-0.167-0.505c0-0.468,0.381-0.849,0.851-0.849c0.469,0,0.85,0.381,0.85,0.849
|
||||
c0,0.282-0.139,0.533-0.352,0.688c-0.141,0.102-0.312,0.163-0.498,0.163C26.18,57.85,25.931,57.713,25.776,57.503z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M37.713,43.596c-0.112-0.412-0.223-0.811-0.322-1.163c1.171-1.037,2.982-2.692,3.487-3.29
|
||||
c0.075-0.091,0.155-0.188,0.229-0.293c0.225-0.313,0.396-0.68,0.263-1.053c-0.15-0.505-0.729-0.686-1.192-0.798
|
||||
c-0.758-0.185-3.198-0.454-4.754-0.604c-0.628-1.434-1.573-3.514-1.97-4.15c-0.25-0.406-0.599-0.9-1.127-0.887
|
||||
c-0.526-0.014-0.875,0.48-1.127,0.887c-0.394,0.637-1.341,2.717-1.968,4.15c-1.559,0.15-3.996,0.42-4.755,0.604
|
||||
c-0.464,0.112-1.042,0.293-1.192,0.798c-0.131,0.373,0.04,0.739,0.263,1.053c0.074,0.104,0.154,0.202,0.231,0.293
|
||||
c0.504,0.598,2.313,2.253,3.484,3.29c-0.1,0.353-0.21,0.751-0.322,1.163c-0.366,1.352-0.751,2.859-0.829,3.45
|
||||
c-0.045,0.343-0.078,0.754,0.064,1.077c0.055,0.123,0.134,0.232,0.248,0.322c0.399,0.345,0.981,0.184,1.433,0.028
|
||||
c0.766-0.261,3.091-1.417,4.47-2.128c1.38,0.711,3.707,1.867,4.473,2.128c0.449,0.155,1.033,0.316,1.433-0.028
|
||||
c0.114-0.09,0.193-0.199,0.247-0.322c0.145-0.323,0.11-0.734,0.064-1.077C38.465,46.455,38.081,44.947,37.713,43.596z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M75.575,36.999c-0.76-0.185-3.198-0.456-4.754-0.603c-0.628-1.436-1.574-3.516-1.97-4.152
|
||||
c-0.25-0.406-0.601-0.9-1.127-0.887c-0.526-0.014-0.876,0.48-1.127,0.887c-0.396,0.637-1.342,2.717-1.97,4.152
|
||||
c-1.557,0.146-3.994,0.418-4.754,0.603c-0.464,0.112-1.041,0.292-1.191,0.798c-0.132,0.373,0.038,0.739,0.262,1.053
|
||||
c0.074,0.104,0.153,0.202,0.23,0.293c0.506,0.598,2.314,2.252,3.485,3.29c-0.1,0.353-0.21,0.751-0.321,1.163
|
||||
c-0.367,1.352-0.751,2.859-0.828,3.45c-0.047,0.343-0.081,0.755,0.063,1.079c0.055,0.121,0.133,0.23,0.247,0.32
|
||||
c0.398,0.345,0.982,0.184,1.434,0.028c0.765-0.263,3.092-1.417,4.47-2.128c1.379,0.711,3.706,1.865,4.471,2.128
|
||||
c0.45,0.155,1.035,0.316,1.433-0.028c0.114-0.09,0.192-0.199,0.248-0.323c0.144-0.322,0.109-0.733,0.064-1.076
|
||||
c-0.078-0.591-0.463-2.099-0.83-3.45c-0.112-0.412-0.223-0.811-0.322-1.163c1.171-1.038,2.981-2.692,3.485-3.29
|
||||
c0.077-0.091,0.157-0.188,0.23-0.293c0.225-0.313,0.396-0.68,0.263-1.053C76.616,37.291,76.037,37.111,75.575,36.999z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M41.646,27.572c0.505,0.596,2.313,2.251,3.485,3.289c-0.1,0.353-0.21,0.75-0.322,1.162
|
||||
c-0.367,1.353-0.751,2.862-0.828,3.452c-0.046,0.342-0.081,0.754,0.063,1.077c0.054,0.123,0.133,0.232,0.247,0.32
|
||||
c0.398,0.346,0.982,0.184,1.434,0.029c0.765-0.262,3.091-1.416,4.47-2.129c1.379,0.713,3.707,1.867,4.471,2.129
|
||||
c0.45,0.154,1.034,0.316,1.434-0.029c0.114-0.088,0.192-0.197,0.248-0.32c0.144-0.323,0.109-0.735,0.064-1.077
|
||||
c-0.078-0.59-0.463-2.1-0.831-3.452c-0.112-0.412-0.223-0.81-0.322-1.162c1.172-1.038,2.98-2.693,3.486-3.289
|
||||
c0.077-0.09,0.156-0.189,0.23-0.293c0.223-0.314,0.394-0.681,0.262-1.053c-0.15-0.506-0.728-0.686-1.191-0.799
|
||||
c-0.759-0.185-3.198-0.454-4.754-0.602c-0.628-1.437-1.574-3.518-1.97-4.154c-0.25-0.404-0.6-0.899-1.127-0.885
|
||||
c-0.526-0.015-0.877,0.48-1.127,0.885c-0.396,0.637-1.342,2.718-1.97,4.154c-1.556,0.147-3.994,0.417-4.754,0.602
|
||||
c-0.463,0.113-1.043,0.293-1.191,0.799c-0.133,0.371,0.038,0.738,0.263,1.053C41.489,27.383,41.568,27.482,41.646,27.572z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M50.058,60.726c0.349,0,0.688,0.058,1.006,0.16c-0.317-0.103-0.653-0.16-1.004-0.16
|
||||
C50.059,60.726,50.059,60.726,50.058,60.726z'
|
||||
/>
|
||||
<path
|
||||
fill='#018B21'
|
||||
d='M60,57h-0.164c-0.035-0.015-0.071-0.032-0.104-0.045c0.002,0.011-0.116,0.021-0.114,0.031
|
||||
c-0.057-0.021-0.175-0.041-0.234-0.061c-0.789-4.52-4.337-7.609-8.736-7.841c-4.457-0.241-8.334,3.372-9.571,7.915H41
|
||||
c-0.552,0-1,0.447-1,1c0,0.035,0.017,0.065,0.02,0.101C40.016,58.148,40,58.188,40,58.239v1.208v14.64
|
||||
C40,74.849,40.429,76,41.19,76h17.672C59.623,76,61,74.849,61,74.087v-14.64v-1.208c0-0.046-0.012-0.091-0.021-0.136
|
||||
C60.983,58.068,61,58.037,61,58C61,57.447,60.553,57,60,57z M50.167,50.051c0.064,0,0.348-0.03,0.412-0.027
|
||||
c3.563,0.189,6.564,3.186,7.437,6.945c-0.316-0.023-0.664,0.005-0.997,0.031h-0.467c-0.802-3.156-3.2-4.947-6.074-5.101
|
||||
c-2.974-0.148-5.556,1.729-6.611,5.101h-1.433C43.521,52.61,46.591,50.051,50.167,50.051z M45.341,57
|
||||
c0.955-2.308,2.889-3.381,5.065-3.274c2.008,0.107,3.688,1.016,4.505,3.274H45.341z'
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
fill='#91EA9A'
|
||||
d='M53.719,63.898c0-1.934-1.567-3.501-3.501-3.501c-1.934,0-3.501,1.567-3.501,3.501
|
||||
c0,0.287,0.044,0.562,0.109,0.829c0.007-0.053,0.011-0.109,0.02-0.156c0.027,0.152,0.066,0.305,0.113,0.455
|
||||
c0.111,0.354,0.279,0.69,0.486,0.988c0.218,0.285,0.473,0.541,0.767,0.748c0.222,0.131,0.466,0.205,0.724,0.223
|
||||
c-0.125,0.683-1.04,5.233-1.04,5.233s-0.301,1.079,2.058,0.929l2.61-0.126c0,0,0.683,0.309-1.294-5.806
|
||||
C52.685,66.767,53.719,65.462,53.719,63.898z'
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgePrivacy;
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function BasqueCountryFlag(props) {
|
||||
const BasqueCountryFlag: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 50 28'>
|
||||
<path d='M0,0 v28 h50 v-28 z' fill='#D52B1E' />
|
||||
@ -9,4 +9,6 @@ export default function BasqueCountryFlag(props) {
|
||||
<path d='M25,0 v28 M0,14 h50' stroke='#fff' strokeWidth='4.3' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BasqueCountryFlag;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function BitcoinIcon(props) {
|
||||
const Bitcoin: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 512 512'>
|
||||
<path d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-141.651-35.33c4.937-32.999-20.191-50.739-54.55-62.573l11.146-44.702-27.213-6.781-10.851 43.524c-7.154-1.783-14.502-3.464-21.803-5.13l10.929-43.81-27.198-6.781-11.153 44.686c-5.922-1.349-11.735-2.682-17.377-4.084l.031-.14-37.53-9.37-7.239 29.062s20.191 4.627 19.765 4.913c11.022 2.751 13.014 10.044 12.68 15.825l-12.696 50.925c.76.194 1.744.473 2.829.907-.907-.225-1.876-.473-2.876-.713l-17.796 71.338c-1.349 3.348-4.767 8.37-12.471 6.464.271.395-19.78-4.937-19.78-4.937l-13.51 31.147 35.414 8.827c6.588 1.651 13.045 3.379 19.4 5.006l-11.262 45.213 27.182 6.781 11.153-44.733a1038.209 1038.209 0 0 0 21.687 5.627l-11.115 44.523 27.213 6.781 11.262-45.128c46.404 8.781 81.299 5.239 95.986-36.727 11.836-33.79-.589-53.281-25.004-65.991 17.78-4.098 31.174-15.792 34.747-39.949zm-62.177 87.179c-8.41 33.79-65.308 15.523-83.755 10.943l14.944-59.899c18.446 4.603 77.6 13.717 68.811 48.956zm8.417-87.667c-7.673 30.736-55.031 15.12-70.393 11.292l13.548-54.327c15.363 3.828 64.836 10.973 56.845 43.035z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Bitcoin;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function BitcoinSignIcon(props) {
|
||||
const BitcoinSign: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 320 512'>
|
||||
<path d='M48 32C48 14.33 62.33 0 80 0C97.67 0 112 14.33 112 32V64H144V32C144 14.33 158.3 0 176 0C193.7 0 208 14.33 208 32V64C208 65.54 207.9 67.06 207.7 68.54C254.1 82.21 288 125.1 288 176C288 200.2 280.3 222.6 267.3 240.9C298.9 260.7 320 295.9 320 336C320 397.9 269.9 448 208 448V480C208 497.7 193.7 512 176 512C158.3 512 144 497.7 144 480V448H112V480C112 497.7 97.67 512 80 512C62.33 512 48 497.7 48 480V448H41.74C18.69 448 0 429.3 0 406.3V101.6C0 80.82 16.82 64 37.57 64H48V32zM176 224C202.5 224 224 202.5 224 176C224 149.5 202.5 128 176 128H64V224H176zM64 288V384H208C234.5 384 256 362.5 256 336C256 309.5 234.5 288 208 288H64z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BitcoinSign;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function BuySatsIcon(props) {
|
||||
const BuySats: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
|
||||
<g>
|
||||
@ -134,4 +134,6 @@ export default function BuySatsIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BuySats;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function BuySatsCheckedIcon(props) {
|
||||
const BuySatsChecked: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
|
||||
<g>
|
||||
@ -76,4 +76,6 @@ export default function BuySatsCheckedIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BuySatsChecked;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function CataloniaFlag(props) {
|
||||
const CataloniaFlag: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 810 540'>
|
||||
<rect width='810' height='540' fill='#FCDD09' />
|
||||
<path stroke='#DA121A' strokeWidth='60' d='M0,90H810m0,120H0m0,120H810m0,120H0' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CataloniaFlag;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function EarthIcon(props) {
|
||||
const Earth: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 440.45 440.45'>
|
||||
<g id='XMLID_34_'>
|
||||
@ -61,11 +61,13 @@ export default function EarthIcon(props) {
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
style={{ opacity: '0.3', fill: '#808080', enableBackground: 'new' }}
|
||||
style={{ opacity: '0.3', fill: '#808080' }}
|
||||
d='M190.23,0.005c5.8,0,11.54,0.26,17.2,0.77
|
||||
C110.48,9.515,34.5,90.995,34.5,190.225c0,99.25,76.01,180.74,172.99,189.45c-5.68,0.51-11.44,0.77-17.26,0.77
|
||||
C85.17,380.445,0,295.285,0,190.225S85.17,0.005,190.23,0.005z'
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Earth;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function ExportIcon(props) {
|
||||
const Export: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 576 512'>
|
||||
<path d='M192 312C192 298.8 202.8 288 216 288H384V160H256c-17.67 0-32-14.33-32-32L224 0H48C21.49 0 0 21.49 0 48v416C0 490.5 21.49 512 48 512h288c26.51 0 48-21.49 48-48v-128H216C202.8 336 192 325.3 192 312zM256 0v128h128L256 0zM568.1 295l-80-80c-9.375-9.375-24.56-9.375-33.94 0s-9.375 24.56 0 33.94L494.1 288H384v48h110.1l-39.03 39.03C450.3 379.7 448 385.8 448 392s2.344 12.28 7.031 16.97c9.375 9.375 24.56 9.375 33.94 0l80-80C578.3 319.6 578.3 304.4 568.1 295z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Export;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function GoldIcon(props) {
|
||||
const Gold: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 511.882 511.882'>
|
||||
<polygon
|
||||
@ -45,4 +45,6 @@ export default function GoldIcon(props) {
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Gold;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function NewTabIcon(props) {
|
||||
const NewTab: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 448 512'>
|
||||
<path d='M256 64C256 46.33 270.3 32 288 32H415.1C415.1 32 415.1 32 415.1 32C420.3 32 424.5 32.86 428.2 34.43C431.1 35.98 435.5 38.27 438.6 41.3C438.6 41.35 438.6 41.4 438.7 41.44C444.9 47.66 447.1 55.78 448 63.9C448 63.94 448 63.97 448 64V192C448 209.7 433.7 224 416 224C398.3 224 384 209.7 384 192V141.3L214.6 310.6C202.1 323.1 181.9 323.1 169.4 310.6C156.9 298.1 156.9 277.9 169.4 265.4L338.7 96H288C270.3 96 256 81.67 256 64V64zM0 128C0 92.65 28.65 64 64 64H160C177.7 64 192 78.33 192 96C192 113.7 177.7 128 160 128H64V416H352V320C352 302.3 366.3 288 384 288C401.7 288 416 302.3 416 320V416C416 451.3 387.3 480 352 480H64C28.65 480 0 451.3 0 416V128z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NewTab;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
// By SatsCoffee https://github.com/satscoffee/nostr_icons/blob/main/nostr_logo_blk.svg
|
||||
export default function NostrIcon(props) {
|
||||
const Nostr: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} viewBox='0 0 875 875'>
|
||||
<path d='m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Nostr;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function RoboSatsIcon(props) {
|
||||
const RoboSats: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 900'>
|
||||
<g>
|
||||
@ -102,4 +102,6 @@ export default function RoboSatsIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RoboSats;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function RoboSatsNoTextIcon(props) {
|
||||
const RoboSatsNoText: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' width='1000px' height='1000px' viewBox='0 0 1000 800'>
|
||||
<g>
|
||||
@ -39,4 +39,6 @@ export default function RoboSatsNoTextIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RoboSatsNoText;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function RoboSatsTextIcon(props) {
|
||||
const RoboSatsText: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' width='2000px' height='500px' viewBox='0 620 2000 1'>
|
||||
<g>
|
||||
@ -95,4 +95,6 @@ export default function RoboSatsTextIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RoboSatsText;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function SellSatsIcon(props) {
|
||||
const SellSats: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
|
||||
<g>
|
||||
@ -135,4 +135,6 @@ export default function SellSatsIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SellSats;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function SellSatsCheckedIcon(props) {
|
||||
const SellSatsChecked: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
|
||||
<g>
|
||||
@ -79,4 +79,6 @@ export default function SellSatsCheckedIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SellSatsChecked;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function SendReceiveIcon(props) {
|
||||
const SendReceive: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 300 300'>
|
||||
<g>
|
||||
@ -20,4 +20,6 @@ export default function SendReceiveIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SendReceive;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function SimplexIcon(props) {
|
||||
const Simplex: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 1080 1080'>
|
||||
<g transform='matrix(4.68 0 0 4.68 668.81 540.67)'>
|
||||
@ -18,4 +18,6 @@ export default function SimplexIcon(props) {
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Simplex;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function TorIcon(props) {
|
||||
const Tor: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 180 180'>
|
||||
<path d='M90.1846205,163.631147 L90.1846205,152.721073 C124.743583,152.621278 152.726063,124.581416 152.726063,89.9975051 C152.726063,55.4160892 124.743583,27.3762266 90.1846205,27.2764318 L90.1846205,16.366358 C130.768698,16.4686478 163.633642,49.3909741 163.633642,89.9975051 C163.633642,130.606531 130.768698,163.531352 90.1846205,163.631147 Z M90.1846205,125.444642 C109.677053,125.342352 125.454621,109.517381 125.454621,89.9975051 C125.454621,70.4801242 109.677053,54.6551533 90.1846205,54.5528636 L90.1846205,43.6452847 C115.704663,43.7450796 136.364695,64.4550091 136.364695,89.9975051 C136.364695,115.542496 115.704663,136.252426 90.1846205,136.35222 L90.1846205,125.444642 Z M90.1846205,70.9167267 C100.640628,71.0165216 109.090758,79.5165493 109.090758,89.9975051 C109.090758,100.480956 100.640628,108.980984 90.1846205,109.080778 L90.1846205,70.9167267 Z M0,89.9975051 C0,139.705328 40.2921772,180 90,180 C139.705328,180 180,139.705328 180,89.9975051 C180,40.2921772 139.705328,0 90,0 C40.2921772,0 0,40.2921772 0,89.9975051 Z'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Tor;
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon } from '@mui/material';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
export default function UserNinjaIcon(props) {
|
||||
const UserNinja: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon {...props} x='0px' y='0px' viewBox='0 0 512 512'>
|
||||
<path d='M64 192c27.25 0 51.75-11.5 69.25-29.75c15 54 64 93.75 122.8 93.75c70.75 0 127.1-57.25 127.1-128s-57.25-128-127.1-128c-50.38 0-93.63 29.38-114.5 71.75C124.1 47.75 96 32 64 32c0 33.37 17.12 62.75 43.13 80C81.13 129.3 64 158.6 64 192zM208 96h95.1C321.7 96 336 110.3 336 128h-160C176 110.3 190.3 96 208 96zM337.8 306.9L256 416L174.2 306.9C93.36 321.6 32 392.2 32 477.3c0 19.14 15.52 34.67 34.66 34.67H445.3c19.14 0 34.66-15.52 34.66-34.67C480 392.2 418.6 321.6 337.8 306.9z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default UserNinja;
|
||||
|
@ -1,6 +1,7 @@
|
||||
export { default as AmbossIcon } from './Amboss';
|
||||
export { default as BitcoinIcon } from './Bitcoin';
|
||||
export { default as BitcoinSignIcon } from './BitcoinSign';
|
||||
export { default as NostrIcon } from './Nostr';
|
||||
export { default as BuySatsIcon } from './BuySats';
|
||||
export { default as BuySatsCheckedIcon } from './BuySatsChecked';
|
||||
export { default as EarthIcon } from './Earth';
|
||||
@ -16,7 +17,13 @@ export { default as ExportIcon } from './Export';
|
||||
export { default as UserNinjaIcon } from './UserNinja';
|
||||
export { default as TorIcon } from './Tor';
|
||||
export { default as SimplexIcon } from './Simplex';
|
||||
export { default as NostrIcon } from './Nostr';
|
||||
|
||||
// Badges
|
||||
export { default as BadgeFounder } from './BadgeFounder';
|
||||
export { default as BadgeDevFund } from './BadgeDevFund';
|
||||
export { default as BadgePrivacy } from './BadgePrivacy';
|
||||
export { default as BadgeLimits } from './BadgeLimits';
|
||||
export { default as BadgeLoved } from './BadgeLoved';
|
||||
|
||||
// Flags with props
|
||||
export { default as FlagWithProps } from './WorldFlags';
|
||||
|
@ -16,7 +16,7 @@ import RangeSlider from './RangeSlider';
|
||||
import currencyDict from '../../../static/assets/currencies.json';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
const RangeThumbComponent = function (props: object) {
|
||||
const RangeThumbComponent: React.FC<React.PropsWithChildren> = (props) => {
|
||||
const { children, ...other } = props;
|
||||
return (
|
||||
<SliderThumb {...other}>
|
||||
@ -34,16 +34,20 @@ interface AmountRangeProps {
|
||||
type: number;
|
||||
currency: number;
|
||||
handleRangeAmountChange: (e: any, activeThumb: any) => void;
|
||||
handleMaxAmountChange: () => void;
|
||||
handleMinAmountChange: () => void;
|
||||
handleCurrencyChange: () => void;
|
||||
handleMaxAmountChange: (
|
||||
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => void;
|
||||
handleMinAmountChange: (
|
||||
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => void;
|
||||
handleCurrencyChange: (newCurrency: number) => void;
|
||||
maxAmountError: boolean;
|
||||
minAmountError: boolean;
|
||||
currencyCode: string;
|
||||
amountLimits: number[];
|
||||
}
|
||||
|
||||
function AmountRange({
|
||||
const AmountRange: React.FC<AmountRangeProps> = ({
|
||||
minAmount,
|
||||
handleRangeAmountChange,
|
||||
currency,
|
||||
@ -55,7 +59,7 @@ function AmountRange({
|
||||
maxAmountError,
|
||||
handleMinAmountChange,
|
||||
handleMaxAmountChange,
|
||||
}: AmountRangeProps) {
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -130,10 +134,10 @@ function AmountRange({
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={currency == 0 ? 1 : currency}
|
||||
value={currency === 0 ? 1 : currency}
|
||||
renderValue={() => currencyCode}
|
||||
onChange={(e) => {
|
||||
handleCurrencyChange(e.target.value);
|
||||
handleCurrencyChange(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{Object.entries(currencyDict).map(([key, value]) => (
|
||||
@ -188,6 +192,6 @@ function AmountRange({
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AmountRange;
|
||||
|
@ -2,7 +2,16 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useAutocomplete from '@mui/base/useAutocomplete';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Button, Fade, Tooltip, Typography, Grow, useTheme } from '@mui/material';
|
||||
import {
|
||||
Button,
|
||||
Fade,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Grow,
|
||||
useTheme,
|
||||
type SxProps,
|
||||
type Theme,
|
||||
} from '@mui/material';
|
||||
import { fiatMethods, swapMethods, PaymentIcon } from '../PaymentMethods';
|
||||
|
||||
// Icons
|
||||
@ -20,12 +29,18 @@ const Root = styled('div')(
|
||||
const Label = styled('label')(
|
||||
({ theme, error, sx }) => `
|
||||
color: ${
|
||||
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#cfcfcf') : error ? '#dd0000' : '#717171'
|
||||
theme.palette.mode === 'dark'
|
||||
? error === true
|
||||
? '#f44336'
|
||||
: '#cfcfcf'
|
||||
: error === true
|
||||
? '#dd0000'
|
||||
: '#717171'
|
||||
};
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
left: 1em;
|
||||
top: ${sx.top};
|
||||
top: ${String(sx.top) ?? '0.72em'};
|
||||
maxHeight: 0em;
|
||||
height: 0em;
|
||||
white-space: no-wrap;
|
||||
@ -35,14 +50,20 @@ const Label = styled('label')(
|
||||
|
||||
const InputWrapper = styled('div')(
|
||||
({ theme, error, sx }) => `
|
||||
min-height: ${sx.minHeight};
|
||||
max-height: ${sx.maxHeight};
|
||||
min-height: ${String(sx.minHeight)};
|
||||
max-height: ${String(sx.maxHeight)};
|
||||
border: 1px solid ${
|
||||
theme.palette.mode === 'dark' ? (error ? '#f44336' : '#434343') : error ? '#dd0000' : '#c4c4c4'
|
||||
theme.palette.mode === 'dark'
|
||||
? error === ''
|
||||
? '#f44336'
|
||||
: '#434343'
|
||||
: error === ''
|
||||
? '#dd0000'
|
||||
: '#c4c4c4'
|
||||
};
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
|
||||
border-radius: 4px;
|
||||
border-color: ${sx.borderColor ? `border-color ${sx.borderColor}` : ''}
|
||||
border-color: ${sx.borderColor !== undefined ? `border-color ${String(sx.borderColor)}` : ''}
|
||||
padding: 1px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -52,10 +73,10 @@ const InputWrapper = styled('div')(
|
||||
&:hover {
|
||||
border-color: ${
|
||||
theme.palette.mode === 'dark'
|
||||
? error
|
||||
? error === true
|
||||
? '#f44336'
|
||||
: sx.hoverBorderColor
|
||||
: error
|
||||
: String(sx.hoverBorderColor)
|
||||
: error === true
|
||||
? '#dd0000'
|
||||
: '#2f2f2f'
|
||||
};
|
||||
@ -64,10 +85,10 @@ const InputWrapper = styled('div')(
|
||||
&.focused {
|
||||
border: 2px solid ${
|
||||
theme.palette.mode === 'dark'
|
||||
? error
|
||||
? error === true
|
||||
? '#f44336'
|
||||
: '#90caf9'
|
||||
: error
|
||||
: error === true
|
||||
? '#dd0000'
|
||||
: '#1976d2'
|
||||
};
|
||||
@ -97,7 +118,8 @@ interface TagProps {
|
||||
onDelete: () => void;
|
||||
onClick: () => void;
|
||||
}
|
||||
const Tag = ({ label, icon, onDelete, onClick, ...other }: TagProps) => {
|
||||
|
||||
const Tag: React.FC<TagProps> = ({ label, icon, onDelete, onClick, ...other }) => {
|
||||
const theme = useTheme();
|
||||
const iconSize = 1.5 * theme.typography.fontSize;
|
||||
return (
|
||||
@ -117,7 +139,7 @@ const StyledTag = styled(Tag)(
|
||||
({ theme, sx }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${sx.height};
|
||||
height: ${String(sx?.height ?? '2.1em')};
|
||||
margin: 2px;
|
||||
line-height: 1.5em;
|
||||
background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'};
|
||||
@ -163,7 +185,7 @@ const ListHeader = styled('span')(
|
||||
|
||||
const Listbox = styled('ul')(
|
||||
({ theme, sx }) => `
|
||||
width: ${sx != null ? sx.width : '15.6em'};
|
||||
width: ${String(sx?.width ?? '15.6em')};
|
||||
margin: 2px 0 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
@ -209,7 +231,23 @@ const Listbox = styled('ul')(
|
||||
`,
|
||||
);
|
||||
|
||||
export default function AutocompletePayments(props) {
|
||||
interface AutocompletePaymentsProps {
|
||||
value: string;
|
||||
optionsType: 'fiat' | 'swap';
|
||||
onAutocompleteChange: (value: string) => void;
|
||||
tooltipTitle: string;
|
||||
labelProps: any;
|
||||
tagProps: any;
|
||||
listBoxProps: any;
|
||||
error: string;
|
||||
label: string;
|
||||
sx: SxProps<Theme>;
|
||||
addNewButtonText: string;
|
||||
isFilter: boolean;
|
||||
listHeaderText: string;
|
||||
}
|
||||
|
||||
const AutocompletePayments: React.FC<AutocompletePaymentsProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
getRootProps,
|
||||
@ -220,19 +258,21 @@ export default function AutocompletePayments(props) {
|
||||
getOptionProps,
|
||||
groupedOptions,
|
||||
value,
|
||||
focused = 'true',
|
||||
focused = true,
|
||||
setAnchorEl,
|
||||
} = useAutocomplete({
|
||||
fullWidth: true,
|
||||
id: 'payment-methods',
|
||||
multiple: true,
|
||||
value: props.value,
|
||||
options: props.optionsType == 'fiat' ? fiatMethods : swapMethods,
|
||||
options: props.optionsType === 'fiat' ? fiatMethods : swapMethods,
|
||||
getOptionLabel: (option) => option.name,
|
||||
onInputChange: (e) => {
|
||||
setVal(e ? (e.target.value ? e.target.value : '') : '');
|
||||
setVal(e.target.value ?? '');
|
||||
},
|
||||
onChange: (event, value) => {
|
||||
props.onAutocompleteChange(value);
|
||||
},
|
||||
onChange: (event, value) => props.onAutocompleteChange(value),
|
||||
onClose: () => {
|
||||
setVal(() => '');
|
||||
},
|
||||
@ -243,36 +283,35 @@ export default function AutocompletePayments(props) {
|
||||
const theme = useTheme();
|
||||
const iconSize = 1.5 * theme.typography.fontSize;
|
||||
|
||||
function handleAddNew(inputProps) {
|
||||
function handleAddNew(inputProps: any): void {
|
||||
fiatMethods.push({ name: inputProps.value, icon: 'custom' });
|
||||
const a = value.push({ name: inputProps.value, icon: 'custom' });
|
||||
setVal(() => '');
|
||||
|
||||
if (a || a == null) {
|
||||
if (a !== undefined) {
|
||||
props.onAutocompleteChange(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={props.tooltipTitle == '' ? 99999 : 300}
|
||||
enterDelay={props.tooltipTitle == '' ? 99999 : 700}
|
||||
enterTouchDelay={props.tooltipTitle === '' ? 99999 : 300}
|
||||
enterDelay={props.tooltipTitle === '' ? 99999 : 700}
|
||||
enterNextDelay={2000}
|
||||
title={props.tooltipTitle}
|
||||
>
|
||||
<div {...getRootProps()}>
|
||||
<Fade
|
||||
appear={false}
|
||||
in={fewerOptions.length == 0 && value.length == 0 && val.length == 0}
|
||||
in={fewerOptions.length === 0 && value.length === 0 && val.length === 0}
|
||||
>
|
||||
<div style={{ height: 0, display: 'flex', alignItems: 'flex-start' }}>
|
||||
<Label
|
||||
{...getInputLabelProps()}
|
||||
sx={{ top: '0.72em', ...(props.labelProps ? props.labelProps.sx : {}) }}
|
||||
error={props.error ? 'error' : null}
|
||||
sx={{ top: '0.72em', ...(props.labelProps?.sx ?? {}) }}
|
||||
error={Boolean(props.error)}
|
||||
>
|
||||
{props.label}
|
||||
</Label>
|
||||
@ -280,7 +319,7 @@ export default function AutocompletePayments(props) {
|
||||
</Fade>
|
||||
<InputWrapper
|
||||
ref={setAnchorEl}
|
||||
error={props.error ? 'error' : null}
|
||||
error={Boolean(props.error)}
|
||||
className={focused ? 'focused' : ''}
|
||||
sx={{
|
||||
minHeight: '2.9em',
|
||||
@ -291,10 +330,11 @@ export default function AutocompletePayments(props) {
|
||||
>
|
||||
{value.map((option, index) => (
|
||||
<StyledTag
|
||||
key={index}
|
||||
label={t(option.name)}
|
||||
icon={option.icon}
|
||||
sx={{ height: '2.1em', ...(props.tagProps ? props.tagProps.sx : {}) }}
|
||||
onClick={props.onClick}
|
||||
sx={{ height: '2.1em', ...(props.tagProps ?? {}) }}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))}
|
||||
@ -303,7 +343,7 @@ export default function AutocompletePayments(props) {
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Grow in={fewerOptions.length > 0}>
|
||||
<Listbox sx={props.listBoxProps ? props.listBoxProps.sx : undefined} {...getListboxProps()}>
|
||||
<Listbox sx={props.listBoxProps?.sx ?? undefined} {...getListboxProps()}>
|
||||
{!props.isFilter ? (
|
||||
<div
|
||||
style={{
|
||||
@ -341,7 +381,13 @@ export default function AutocompletePayments(props) {
|
||||
))}
|
||||
{val != null || !props.isFilter ? (
|
||||
val.length > 2 ? (
|
||||
<Button size='small' fullWidth={true} onClick={() => handleAddNew(getInputProps())}>
|
||||
<Button
|
||||
size='small'
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
handleAddNew(getInputProps());
|
||||
}}
|
||||
>
|
||||
<DashboardCustomizeIcon sx={{ width: '1em', height: '1em' }} />
|
||||
{props.addNewButtonText}
|
||||
</Button>
|
||||
@ -353,7 +399,12 @@ export default function AutocompletePayments(props) {
|
||||
{/* Here goes what happens if there is no fewerOptions */}
|
||||
<Grow in={getInputProps().value.length > 0 && !props.isFilter && fewerOptions.length === 0}>
|
||||
<Listbox {...getListboxProps()}>
|
||||
<Button fullWidth={true} onClick={() => handleAddNew(getInputProps())}>
|
||||
<Button
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
handleAddNew(getInputProps());
|
||||
}}
|
||||
>
|
||||
<DashboardCustomizeIcon sx={{ width: '1.28em', height: '1.28em' }} />
|
||||
{props.addNewButtonText}
|
||||
</Button>
|
||||
@ -361,4 +412,6 @@ export default function AutocompletePayments(props) {
|
||||
</Grow>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AutocompletePayments;
|
||||
|
@ -40,8 +40,11 @@ import { amountToString, computeSats, pn } from '../../utils';
|
||||
|
||||
import { SelfImprovement, Lock, HourglassTop, DeleteSweep, Edit, Map } from '@mui/icons-material';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { fiatMethods } from '../PaymentMethods';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import SelectCoordinator from './SelectCoordinator';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface MakerFormProps {
|
||||
disableRequest?: boolean;
|
||||
@ -50,7 +53,7 @@ interface MakerFormProps {
|
||||
onSubmit?: () => void;
|
||||
onReset?: () => void;
|
||||
submitButtonLabel?: string;
|
||||
onOrderCreated?: (id: number) => void;
|
||||
onOrderCreated?: (shortAlias: string, id: number) => void;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
@ -64,8 +67,10 @@ const MakerForm = ({
|
||||
onOrderCreated = () => null,
|
||||
onClickGenerateRobot = () => null,
|
||||
}: MakerFormProps): JSX.Element => {
|
||||
const { fav, setFav, limits, fetchLimits, info, maker, setMaker, baseUrl, robot } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { fav, setFav, settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, focusedCoordinator, coordinatorUpdatedAt, federationUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { maker, setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -80,30 +85,31 @@ const MakerForm = ({
|
||||
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
|
||||
const [submittingRequest, setSubmittingRequest] = useState<boolean>(false);
|
||||
const [amountRangeEnabled, setAmountRangeEnabled] = useState<boolean>(true);
|
||||
const [limits, setLimits] = useState<LimitList>({});
|
||||
|
||||
const maxRangeAmountMultiple = 14.8;
|
||||
const minRangeAmountMultiple = 1.6;
|
||||
const amountSafeThresholds = [1.03, 0.98];
|
||||
|
||||
useEffect(() => {
|
||||
setCurrencyCode(currencyDict[fav.currency == 0 ? 1 : fav.currency]);
|
||||
if (Object.keys(limits.list).length === 0) {
|
||||
fetchLimits().then((data) => {
|
||||
updateAmountLimits(data, fav.currency, maker.premium);
|
||||
updateCurrentPrice(data, fav.currency, maker.premium);
|
||||
updateSatoshisLimits(data);
|
||||
});
|
||||
} else {
|
||||
updateAmountLimits(limits.list, fav.currency, maker.premium);
|
||||
updateCurrentPrice(limits.list, fav.currency, maker.premium);
|
||||
updateSatoshisLimits(limits.list);
|
||||
|
||||
fetchLimits();
|
||||
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
|
||||
if (focusedCoordinator) {
|
||||
const newLimits = federation.getCoordinator(focusedCoordinator).limits;
|
||||
if (Object.keys(newLimits).length !== 0) {
|
||||
updateAmountLimits(newLimits, fav.currency, maker.premium);
|
||||
updateCurrentPrice(newLimits, fav.currency, maker.premium);
|
||||
updateSatoshisLimits(newLimits);
|
||||
setLimits(newLimits);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
}, [coordinatorUpdatedAt]);
|
||||
|
||||
const updateAmountLimits = function (limitList: LimitList, currency: number, premium: number) {
|
||||
const index = currency == 0 ? 1 : currency;
|
||||
const updateAmountLimits = function (
|
||||
limitList: LimitList,
|
||||
currency: number,
|
||||
premium: number,
|
||||
): void {
|
||||
const index = currency === 0 ? 1 : currency;
|
||||
let minAmountLimit: number = limitList[index].min_amount * (1 + premium / 100);
|
||||
let maxAmountLimit: number = limitList[index].max_amount * (1 + premium / 100);
|
||||
|
||||
@ -113,24 +119,28 @@ const MakerForm = ({
|
||||
setAmountLimits([minAmountLimit, maxAmountLimit]);
|
||||
};
|
||||
|
||||
const updateSatoshisLimits = function (limitList: LimitList) {
|
||||
const updateSatoshisLimits = function (limitList: LimitList): void {
|
||||
const minAmount: number = limitList[1000].min_amount * 100000000;
|
||||
const maxAmount: number = limitList[1000].max_amount * 100000000;
|
||||
setSatoshisLimits([minAmount, maxAmount]);
|
||||
};
|
||||
|
||||
const updateCurrentPrice = function (limitsList: LimitList, currency: number, premium: number) {
|
||||
const index = currency == 0 ? 1 : currency;
|
||||
const updateCurrentPrice = function (
|
||||
limitsList: LimitList,
|
||||
currency: number,
|
||||
premium: number,
|
||||
): void {
|
||||
const index = currency === 0 ? 1 : currency;
|
||||
let price = '...';
|
||||
if (maker.isExplicit && maker.amount > 0 && maker.satoshis > 0) {
|
||||
price = maker.amount / (maker.satoshis / 100000000);
|
||||
} else if (!maker.is_explicit) {
|
||||
} else if (!maker.isExplicit) {
|
||||
price = limitsList[index].price * (1 + premium / 100);
|
||||
}
|
||||
setCurrentPrice(parseFloat(Number(price).toPrecision(5)));
|
||||
};
|
||||
|
||||
const handleCurrencyChange = function (newCurrency: number) {
|
||||
const handleCurrencyChange = function (newCurrency: number): void {
|
||||
const currencyCode: string = currencyDict[newCurrency];
|
||||
setCurrencyCode(currencyCode);
|
||||
setFav({
|
||||
@ -138,12 +148,12 @@ const MakerForm = ({
|
||||
currency: newCurrency,
|
||||
mode: newCurrency === 1000 ? 'swap' : 'fiat',
|
||||
});
|
||||
updateAmountLimits(limits.list, newCurrency, maker.premium);
|
||||
updateCurrentPrice(limits.list, newCurrency, maker.premium);
|
||||
updateAmountLimits(limits, newCurrency, maker.premium);
|
||||
updateCurrentPrice(limits, newCurrency, maker.premium);
|
||||
|
||||
if (makerHasAmountRange) {
|
||||
const minAmount = parseFloat(Number(limits.list[newCurrency].min_amount).toPrecision(2));
|
||||
const maxAmount = parseFloat(Number(limits.list[newCurrency].max_amount).toPrecision(2));
|
||||
const minAmount = parseFloat(Number(limits[newCurrency].min_amount).toPrecision(2));
|
||||
const maxAmount = parseFloat(Number(limits[newCurrency].max_amount).toPrecision(2));
|
||||
if (
|
||||
parseFloat(maker.minAmount) < minAmount ||
|
||||
parseFloat(maker.minAmount) > maxAmount ||
|
||||
@ -163,10 +173,12 @@ const MakerForm = ({
|
||||
return maker.advancedOptions && amountRangeEnabled;
|
||||
}, [maker.advancedOptions, amountRangeEnabled]);
|
||||
|
||||
const handlePaymentMethodChange = function (paymentArray: { name: string; icon: string }[]) {
|
||||
let includeCoordinates = false;
|
||||
const handlePaymentMethodChange = function (
|
||||
paymentArray: Array<{ name: string; icon: string }>,
|
||||
): void {
|
||||
let str = '';
|
||||
const arrayLength = paymentArray.length;
|
||||
let includeCoordinates = false;
|
||||
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
str += paymentArray[i].name + ' ';
|
||||
@ -190,14 +202,18 @@ const MakerForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleMinAmountChange = function (e) {
|
||||
const handleMinAmountChange = function (
|
||||
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
|
||||
): void {
|
||||
setMaker({
|
||||
...maker,
|
||||
minAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
|
||||
});
|
||||
};
|
||||
|
||||
const handleMaxAmountChange = function (e) {
|
||||
const handleMaxAmountChange = function (
|
||||
e: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>,
|
||||
): void {
|
||||
setMaker({
|
||||
...maker,
|
||||
maxAmount: parseFloat(Number(e.target.value).toPrecision(e.target.value < 100 ? 2 : 3)),
|
||||
@ -205,7 +221,7 @@ const MakerForm = ({
|
||||
};
|
||||
|
||||
const handlePremiumChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> =
|
||||
function ({ target: { value } }) {
|
||||
function ({ target: { value } }): void {
|
||||
const max = fav.mode === 'fiat' ? 999 : 99;
|
||||
const min = -100;
|
||||
const newPremium = Math.floor(Number(value) * Math.pow(10, 2)) / Math.pow(10, 2);
|
||||
@ -218,8 +234,8 @@ const MakerForm = ({
|
||||
badPremiumText = t('Must be more than {{min}}%', { min });
|
||||
premium = -99.99;
|
||||
}
|
||||
updateCurrentPrice(limits.list, fav.currency, premium);
|
||||
updateAmountLimits(limits.list, fav.currency, premium);
|
||||
updateCurrentPrice(limits, fav.currency, premium);
|
||||
updateAmountLimits(limits, fav.currency, premium);
|
||||
setMaker({
|
||||
...maker,
|
||||
premium: isNaN(newPremium) || value === '' ? '' : premium,
|
||||
@ -227,7 +243,7 @@ const MakerForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleSatoshisChange = function (e: object) {
|
||||
const handleSatoshisChange = function (e: object): void {
|
||||
const newSatoshis = e.target.value;
|
||||
let badSatoshisText: string = '';
|
||||
let satoshis: string = newSatoshis;
|
||||
@ -247,14 +263,14 @@ const MakerForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickRelative = function () {
|
||||
const handleClickRelative = function (): void {
|
||||
setMaker({
|
||||
...maker,
|
||||
isExplicit: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickExplicit = function () {
|
||||
const handleClickExplicit = function (): void {
|
||||
if (!maker.advancedOptions) {
|
||||
setMaker({
|
||||
...maker,
|
||||
@ -263,12 +279,24 @@ const MakerForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateOrder = function () {
|
||||
if (!disableRequest) {
|
||||
const handleCreateOrder = function (): void {
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(maker.coordinator)
|
||||
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
|
||||
const auth = {
|
||||
tokenSHA256: garage.getRobot().tokenSHA256,
|
||||
keys: {
|
||||
pubKey: garage.getRobot().pubKey?.split('\n').join('\\'),
|
||||
encPrivKey: garage.getRobot().encPrivKey?.split('\n').join('\\'),
|
||||
},
|
||||
};
|
||||
|
||||
if (!disableRequest && focusedCoordinator) {
|
||||
setSubmittingRequest(true);
|
||||
const body = {
|
||||
type: fav.type == 0 ? 1 : 0,
|
||||
currency: fav.currency == 0 ? 1 : fav.currency,
|
||||
type: fav.type === 0 ? 1 : 0,
|
||||
currency: fav.currency === 0 ? 1 : fav.currency,
|
||||
amount: makerHasAmountRange ? null : maker.amount,
|
||||
has_range: makerHasAmountRange,
|
||||
min_amount: makerHasAmountRange ? maker.minAmount : null,
|
||||
@ -276,7 +304,7 @@ const MakerForm = ({
|
||||
payment_method:
|
||||
maker.paymentMethodsText === '' ? 'not specified' : maker.paymentMethodsText,
|
||||
is_explicit: maker.isExplicit,
|
||||
premium: maker.isExplicit ? null : maker.premium == '' ? 0 : maker.premium,
|
||||
premium: maker.isExplicit ? null : maker.premium === '' ? 0 : maker.premium,
|
||||
satoshis: maker.isExplicit ? maker.satoshis : null,
|
||||
public_duration: maker.publicDuration,
|
||||
escrow_duration: maker.escrowDuration,
|
||||
@ -284,48 +312,54 @@ const MakerForm = ({
|
||||
latitude: maker.latitude,
|
||||
longitude: maker.longitude,
|
||||
};
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 })
|
||||
.then((data: object) => {
|
||||
.post(url, `${basePath}/api/make/`, body, auth)
|
||||
.then((data: any) => {
|
||||
setBadRequest(data.bad_request);
|
||||
if (data.id) {
|
||||
onOrderCreated(data.id);
|
||||
if (data.id !== undefined) {
|
||||
onOrderCreated(maker.coordinator, data.id);
|
||||
}
|
||||
setSubmittingRequest(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setBadRequest('Request error');
|
||||
});
|
||||
}
|
||||
setOpenDialogs(false);
|
||||
};
|
||||
|
||||
const handleChangePublicDuration = function (date: Date) {
|
||||
const handleChangePublicDuration = function (date: Date): void {
|
||||
const d = new Date(date);
|
||||
const hours: number = d.getHours();
|
||||
const minutes: number = d.getMinutes();
|
||||
|
||||
const total_secs: number = hours * 60 * 60 + minutes * 60;
|
||||
const totalSecs: number = hours * 60 * 60 + minutes * 60;
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
publicExpiryTime: date,
|
||||
publicDuration: total_secs,
|
||||
publicDuration: totalSecs,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeEscrowDuration = function (date: Date) {
|
||||
const handleChangeEscrowDuration = function (date: Date): void {
|
||||
const d = new Date(date);
|
||||
const hours: number = d.getHours();
|
||||
const minutes: number = d.getMinutes();
|
||||
|
||||
const total_secs: number = hours * 60 * 60 + minutes * 60;
|
||||
const totalSecs: number = hours * 60 * 60 + minutes * 60;
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
escrowExpiryTime: date,
|
||||
escrowDuration: total_secs,
|
||||
escrowDuration: totalSecs,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickAdvanced = function () {
|
||||
const handleClickAdvanced = function (): void {
|
||||
if (maker.advancedOptions) {
|
||||
handleClickRelative();
|
||||
setMaker({ ...maker, advancedOptions: false });
|
||||
@ -352,14 +386,16 @@ const MakerForm = ({
|
||||
);
|
||||
}, [maker.minAmount, maker.maxAmount, amountLimits]);
|
||||
|
||||
const resetRange = function (advancedOptions: boolean) {
|
||||
const resetRange = function (advancedOptions: boolean): void {
|
||||
const index = fav.currency === 0 ? 1 : fav.currency;
|
||||
const minAmount = maker.amount
|
||||
const minAmount =
|
||||
maker.amount !== ''
|
||||
? parseFloat((maker.amount / 2).toPrecision(2))
|
||||
: parseFloat(Number(limits.list[index].max_amount * 0.25).toPrecision(2));
|
||||
const maxAmount = maker.amount
|
||||
: parseFloat(Number(limits[index].max_amount * 0.25).toPrecision(2));
|
||||
const maxAmount =
|
||||
maker.amount !== ''
|
||||
? parseFloat(maker.amount)
|
||||
: parseFloat(Number(limits.list[index].max_amount * 0.75).toPrecision(2));
|
||||
: parseFloat(Number(limits[index].max_amount * 0.75).toPrecision(2));
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
@ -369,7 +405,7 @@ const MakerForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleRangeAmountChange = function (e: any, newValue, activeThumb: number) {
|
||||
const handleRangeAmountChange = function (e: any, newValue, activeThumb: number): void {
|
||||
let minAmount = e.target.value[0];
|
||||
let maxAmount = e.target.value[1];
|
||||
|
||||
@ -406,11 +442,14 @@ const MakerForm = ({
|
||||
const handleClickAmountRangeEnabled = function (
|
||||
_e: React.ChangeEvent<HTMLInputElement>,
|
||||
checked: boolean,
|
||||
) {
|
||||
): void {
|
||||
setAmountRangeEnabled(checked);
|
||||
};
|
||||
|
||||
const amountLabel = useMemo(() => {
|
||||
if (!focusedCoordinator) return;
|
||||
|
||||
const info = federation.getCoordinator(focusedCoordinator)?.info;
|
||||
const defaultRoutingBudget = 0.001;
|
||||
let label = t('Amount');
|
||||
let helper = '';
|
||||
@ -420,7 +459,7 @@ const MakerForm = ({
|
||||
swapSats = computeSats({
|
||||
amount: Number(maker.amount),
|
||||
premium: Number(maker.premium),
|
||||
fee: -info.maker_fee,
|
||||
fee: -(info?.maker_fee ?? 0),
|
||||
routingBudget: defaultRoutingBudget,
|
||||
});
|
||||
label = t('Onchain amount to send (BTC)');
|
||||
@ -431,7 +470,7 @@ const MakerForm = ({
|
||||
swapSats = computeSats({
|
||||
amount: Number(maker.amount),
|
||||
premium: Number(maker.premium),
|
||||
fee: info.maker_fee,
|
||||
fee: info?.maker_fee ?? 0,
|
||||
});
|
||||
label = t('Onchain amount to receive (BTC)');
|
||||
helper = t('You send approx {{swapSats}} LN Sats (fees might vary)', {
|
||||
@ -440,30 +479,30 @@ const MakerForm = ({
|
||||
}
|
||||
}
|
||||
return { label, helper, swapSats };
|
||||
}, [fav, maker.amount, maker.premium, info]);
|
||||
}, [fav, maker.amount, maker.premium, federationUpdatedAt]);
|
||||
|
||||
const disableSubmit = useMemo(() => {
|
||||
return (
|
||||
fav.type == null ||
|
||||
(!makerHasAmountRange &&
|
||||
maker.amount != '' &&
|
||||
maker.amount !== '' &&
|
||||
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])) ||
|
||||
maker.badPaymentMethod ||
|
||||
(maker.amount == null && (!makerHasAmountRange || limits.loading)) ||
|
||||
(maker.amount == null && (!makerHasAmountRange || Object.keys(limits).lenght < 1)) ||
|
||||
(makerHasAmountRange && (minAmountError || maxAmountError)) ||
|
||||
(!makerHasAmountRange && maker.amount <= 0) ||
|
||||
(maker.isExplicit && (maker.badSatoshisText != '' || maker.satoshis == '')) ||
|
||||
(!maker.isExplicit && maker.badPremiumText != '')
|
||||
(maker.isExplicit && (maker.badSatoshisText !== '' || maker.satoshis === '')) ||
|
||||
(!maker.isExplicit && maker.badPremiumText !== '')
|
||||
);
|
||||
}, [maker, amountLimits, limits, fav.type, makerHasAmountRange]);
|
||||
}, [maker, amountLimits, coordinatorUpdatedAt, fav.type, makerHasAmountRange]);
|
||||
|
||||
const clearMaker = function () {
|
||||
const clearMaker = function (): void {
|
||||
setFav({ ...fav, type: null });
|
||||
setMaker(defaultMaker);
|
||||
};
|
||||
|
||||
const handleAddLocation = (pos: [number, number]) => {
|
||||
if (pos && pos.length === 2) {
|
||||
const handleAddLocation = (pos: [number, number]): void => {
|
||||
if (pos?.length === 2) {
|
||||
setMaker((maker) => {
|
||||
return {
|
||||
...maker,
|
||||
@ -471,10 +510,11 @@ const MakerForm = ({
|
||||
longitude: parseFloat(pos[1].toPrecision(6)),
|
||||
};
|
||||
});
|
||||
if (!maker.paymentMethods.find((method) => method.icon === 'cash')) {
|
||||
const cashMethod = maker.paymentMethods.find((method) => method.icon === 'cash');
|
||||
if (cashMethod !== null) {
|
||||
const newMethods = maker.paymentMethods;
|
||||
const cash = fiatMethods.find((method) => method.icon === 'cash');
|
||||
if (cash) {
|
||||
if (cash !== null) {
|
||||
newMethods.unshift(cash);
|
||||
handlePaymentMethodChange(newMethods);
|
||||
}
|
||||
@ -482,7 +522,7 @@ const MakerForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const SummaryText = function () {
|
||||
const SummaryText = (): JSX.Element => {
|
||||
return (
|
||||
<Typography
|
||||
component='h2'
|
||||
@ -494,7 +534,7 @@ const MakerForm = ({
|
||||
? fav.mode === 'fiat'
|
||||
? t('Order for ')
|
||||
: t('Swap of ')
|
||||
: fav.type == 1
|
||||
: fav.type === 1
|
||||
? fav.mode === 'fiat'
|
||||
? t('Buy BTC for ')
|
||||
: t('Swap into LN ')
|
||||
@ -512,7 +552,7 @@ const MakerForm = ({
|
||||
{' ' + (fav.mode === 'fiat' ? currencyCode : 'Sats')}
|
||||
{maker.isExplicit
|
||||
? t(' of {{satoshis}} Satoshis', { satoshis: pn(maker.satoshis) })
|
||||
: maker.premium == 0
|
||||
: maker.premium === 0
|
||||
? fav.mode === 'fiat'
|
||||
? t(' at market price')
|
||||
: ''
|
||||
@ -531,7 +571,7 @@ const MakerForm = ({
|
||||
setOpenDialogs(false);
|
||||
}}
|
||||
onClickDone={handleCreateOrder}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
hasRobot={garage.getRobot().avatarLoaded}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
<F2fMapDialog
|
||||
@ -542,19 +582,19 @@ const MakerForm = ({
|
||||
message={t(
|
||||
'To protect your privacy, the exact location you pin will be slightly randomized.',
|
||||
)}
|
||||
orderType={fav.type || 0}
|
||||
orderType={fav?.type ?? 0}
|
||||
onClose={(pos?: [number, number]) => {
|
||||
if (pos) handleAddLocation(pos);
|
||||
if (pos != null) handleAddLocation(pos);
|
||||
setOpenWorldmap(false);
|
||||
}}
|
||||
zoom={maker.latitude && maker.longitude ? 6 : undefined}
|
||||
/>
|
||||
<Collapse in={limits.list.length == 0}>
|
||||
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}>
|
||||
<Collapse in={Object.keys(limits).lenght === 0}>
|
||||
<div style={{ display: Object.keys(limits) === 0 ? '' : 'none' }}>
|
||||
<LinearProgress />
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse in={!(limits.list.length == 0 || collapseAll)}>
|
||||
<Collapse in={!(Object.keys(limits).lenght === 0 || collapseAll)}>
|
||||
<Grid container justifyContent='space-between' spacing={0} sx={{ maxHeight: '1em' }}>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
@ -590,7 +630,7 @@ const MakerForm = ({
|
||||
>
|
||||
<Switch
|
||||
size='small'
|
||||
disabled={limits.list.length == 0}
|
||||
disabled={Object.keys(limits).length === 0}
|
||||
checked={maker.advancedOptions}
|
||||
onChange={handleClickAdvanced}
|
||||
/>
|
||||
@ -611,9 +651,9 @@ const MakerForm = ({
|
||||
<FormHelperText sx={{ textAlign: 'center' }}>{t('Swap?')}</FormHelperText>
|
||||
<Checkbox
|
||||
sx={{ position: 'relative', bottom: '0.3em' }}
|
||||
checked={fav.mode == 'swap'}
|
||||
checked={fav.mode === 'swap'}
|
||||
onClick={() => {
|
||||
handleCurrencyChange(fav.mode == 'swap' ? 1 : 1000);
|
||||
handleCurrencyChange(fav.mode === 'swap' ? 1 : 1000);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -636,10 +676,10 @@ const MakerForm = ({
|
||||
type: 1,
|
||||
});
|
||||
}}
|
||||
disableElevation={fav.type == 1}
|
||||
disableElevation={fav.type === 1}
|
||||
sx={{
|
||||
backgroundColor: fav.type == 1 ? 'primary.main' : 'background.paper',
|
||||
color: fav.type == 1 ? 'background.paper' : 'text.secondary',
|
||||
backgroundColor: fav.type === 1 ? 'primary.main' : 'background.paper',
|
||||
color: fav.type === 1 ? 'background.paper' : 'text.secondary',
|
||||
':hover': {
|
||||
color: 'background.paper',
|
||||
},
|
||||
@ -656,11 +696,11 @@ const MakerForm = ({
|
||||
type: 0,
|
||||
});
|
||||
}}
|
||||
disableElevation={fav.type == 0}
|
||||
disableElevation={fav.type === 0}
|
||||
color='secondary'
|
||||
sx={{
|
||||
backgroundColor: fav.type == 0 ? 'secondary.main' : 'background.paper',
|
||||
color: fav.type == 0 ? 'background.secondary' : 'text.secondary',
|
||||
backgroundColor: fav.type === 0 ? 'secondary.main' : 'background.paper',
|
||||
color: fav.type === 0 ? 'background.secondary' : 'text.secondary',
|
||||
':hover': {
|
||||
color: 'background.paper',
|
||||
},
|
||||
@ -730,15 +770,15 @@ const MakerForm = ({
|
||||
disabled={makerHasAmountRange}
|
||||
variant={makerHasAmountRange ? 'filled' : 'outlined'}
|
||||
error={
|
||||
maker.amount != '' &&
|
||||
maker.amount !== '' &&
|
||||
(maker.amount < amountLimits[0] || maker.amount > amountLimits[1])
|
||||
}
|
||||
helperText={
|
||||
maker.amount < amountLimits[0] && maker.amount != ''
|
||||
maker.amount < amountLimits[0] && maker.amount !== ''
|
||||
? t('Must be more than {{minAmount}}', {
|
||||
minAmount: pn(parseFloat(amountLimits[0].toPrecision(2))),
|
||||
})
|
||||
: maker.amount > amountLimits[1] && maker.amount != ''
|
||||
: maker.amount > amountLimits[1] && maker.amount !== ''
|
||||
? t('Must be less than {{maxAmount}}', {
|
||||
maxAmount: pn(parseFloat(amountLimits[1].toPrecision(2))),
|
||||
})
|
||||
@ -761,7 +801,7 @@ const MakerForm = ({
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{fav.mode === 'swap' && maker.amount != '' ? (
|
||||
{fav.mode === 'swap' && maker.amount !== '' ? (
|
||||
<FormHelperText sx={{ textAlign: 'center' }}>
|
||||
{amountLabel.helper}
|
||||
</FormHelperText>
|
||||
@ -780,7 +820,7 @@ const MakerForm = ({
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={fav.currency == 0 ? 1 : fav.currency}
|
||||
value={fav.currency === 0 ? 1 : fav.currency}
|
||||
onChange={(e) => {
|
||||
handleCurrencyChange(e.target.value);
|
||||
}}
|
||||
@ -806,13 +846,15 @@ const MakerForm = ({
|
||||
<Grid item xs={12}>
|
||||
<AutocompletePayments
|
||||
onAutocompleteChange={handlePaymentMethodChange}
|
||||
onClick={() => setOpenWorldmap(true)}
|
||||
onClick={() => {
|
||||
setOpenWorldmap(true);
|
||||
}}
|
||||
optionsType={fav.mode}
|
||||
error={maker.badPaymentMethod}
|
||||
helperText={maker.badPaymentMethod ? t('Must be shorter than 65 characters') : ''}
|
||||
label={fav.mode == 'swap' ? t('Swap Destination(s)') : t('Fiat Payment Method(s)')}
|
||||
label={fav.mode === 'swap' ? t('Swap Destination(s)') : t('Fiat Payment Method(s)')}
|
||||
tooltipTitle={t(
|
||||
fav.mode == 'swap'
|
||||
fav.mode === 'swap'
|
||||
? t('Enter the destination of the Lightning swap')
|
||||
: 'Enter your preferred fiat payment methods. Fast methods are highly recommended.',
|
||||
)}
|
||||
@ -844,7 +886,9 @@ const MakerForm = ({
|
||||
color: theme.palette.text.secondary,
|
||||
borderColor: theme.palette.text.disabled,
|
||||
}}
|
||||
onClick={() => setOpenWorldmap(true)}
|
||||
onClick={() => {
|
||||
setOpenWorldmap(true);
|
||||
}}
|
||||
>
|
||||
{t('Face to Face Location')}
|
||||
<Map style={{ paddingLeft: 5 }} />
|
||||
@ -913,7 +957,7 @@ const MakerForm = ({
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('Satoshis')}
|
||||
error={maker.badSatoshisText != ''}
|
||||
error={maker.badSatoshisText !== ''}
|
||||
helperText={maker.badSatoshisText === '' ? null : maker.badSatoshisText}
|
||||
type='number'
|
||||
required={true}
|
||||
@ -933,7 +977,7 @@ const MakerForm = ({
|
||||
<div style={{ display: maker.isExplicit ? 'none' : '' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
error={maker.badPremiumText != ''}
|
||||
error={maker.badPremiumText !== ''}
|
||||
helperText={maker.badPremiumText === '' ? null : maker.badPremiumText}
|
||||
label={t('Premium over Market (%)')}
|
||||
type='number'
|
||||
@ -1096,6 +1140,15 @@ const MakerForm = ({
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
<SelectCoordinator
|
||||
coordinator={maker.coordinator}
|
||||
setCoordinator={(coordinator) => {
|
||||
setMaker((maker) => {
|
||||
return { ...maker, coordinator };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Grid container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<SummaryText />
|
||||
@ -1152,7 +1205,7 @@ const MakerForm = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Collapse in={!(limits.list.length == 0)}>
|
||||
<Collapse in={!(Object.keys(limits).length === 0)}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
|
125
frontend/src/components/MakerForm/SelectCoordinator.tsx
Normal file
125
frontend/src/components/MakerForm/SelectCoordinator.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
Tooltip,
|
||||
Typography,
|
||||
type SelectChangeEvent,
|
||||
} from '@mui/material';
|
||||
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface SelectCoordinatorProps {
|
||||
coordinator: string;
|
||||
setCoordinator: (coordinator: string) => void;
|
||||
}
|
||||
|
||||
const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({ coordinator, setCoordinator }) => {
|
||||
const { setOpen, hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, setFocusedCoordinator, sortedCoordinators } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickCurrentCoordinator = function (shortAlias: string): void {
|
||||
setFocusedCoordinator(shortAlias);
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: true };
|
||||
});
|
||||
};
|
||||
|
||||
const handleCoordinatorChange = (e: SelectChangeEvent<string>): void => {
|
||||
setCoordinator(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={500}
|
||||
enterDelay={700}
|
||||
enterNextDelay={2000}
|
||||
title={t(
|
||||
'The provider the lightning and communication infrastructure. The host will be in charge of providing support and solving disputes. The trade fees are set by the host. Make sure to only select order hosts that you trust!',
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderRadius: '4px',
|
||||
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.mode === 'dark' ? '#ffffff' : '#2f2f2f',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant='caption' color='text.secondary'>
|
||||
{t('Order Host')}
|
||||
</Typography>
|
||||
|
||||
<Grid container>
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
sx={{ cursor: 'pointer', position: 'relative', left: '0.3em', bottom: '0.1em' }}
|
||||
onClick={() => {
|
||||
onClickCurrentCoordinator(coordinator);
|
||||
}}
|
||||
>
|
||||
<Grid item>
|
||||
<RobotAvatar
|
||||
nickname={coordinator}
|
||||
coordinator={true}
|
||||
style={{ width: '3em', height: '3em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
baseUrl={hostUrl}
|
||||
small={true}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={9}>
|
||||
<Select
|
||||
variant='standard'
|
||||
fullWidth
|
||||
required={true}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={coordinator}
|
||||
onChange={handleCoordinatorChange}
|
||||
disableUnderline
|
||||
>
|
||||
{sortedCoordinators.map((shortAlias: string): JSX.Element | null => {
|
||||
let row: JSX.Element | null = null;
|
||||
if (
|
||||
shortAlias === coordinator ||
|
||||
(federation.getCoordinator(shortAlias).enabled === true &&
|
||||
federation.getCoordinator(shortAlias).info !== undefined)
|
||||
) {
|
||||
row = (
|
||||
<MenuItem key={shortAlias} value={shortAlias}>
|
||||
<Typography>{federation.getCoordinator(shortAlias).longAlias}</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
return row;
|
||||
})}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectCoordinator;
|
@ -2,13 +2,13 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import { apiClient } from '../../services/api';
|
||||
import { MapContainer, GeoJSON, useMapEvents, TileLayer, Tooltip, Marker } from 'react-leaflet';
|
||||
import { useTheme, LinearProgress } from '@mui/material';
|
||||
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { GeoJsonObject } from 'geojson';
|
||||
import { DivIcon, LeafletMouseEvent } from 'leaflet';
|
||||
import { PublicOrder } from '../../models';
|
||||
import { type GeoJsonObject } from 'geojson';
|
||||
import { DivIcon, type LeafletMouseEvent } from 'leaflet';
|
||||
import { type PublicOrder } from '../../models';
|
||||
import OrderTooltip from '../Charts/helpers/OrderTooltip';
|
||||
import getWorldmapGeojson from '../../geo/Web';
|
||||
import MarkerClusterGroup from '@christopherpickering/react-leaflet-markercluster';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
|
||||
interface MapPinProps {
|
||||
fillColor: string;
|
||||
@ -17,7 +17,6 @@ interface MapPinProps {
|
||||
}
|
||||
|
||||
const MapPin = ({ fillColor, outlineColor, eyesColor }: MapPinProps): string => {
|
||||
console.log(fillColor, outlineColor);
|
||||
return `<svg id='robot_pin' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18.66 29.68'><defs><style>.body{fill:${fillColor};}.eyes{fill:${eyesColor};}.outline{fill:${outlineColor};}</style></defs><path class='body' d='M18,8A9.13,9.13,0,0,0,10.89.62,10.88,10.88,0,0,0,9.33.49,10.88,10.88,0,0,0,7.77.62,9.13,9.13,0,0,0,.66,8a12.92,12.92,0,0,0,1.19,8.25C2.68,18.09,7.47,27.6,9.07,29c0,.12.11.19.19.19l.07,0,.07,0c.08,0,.15-.07.19-.19,1.6-1.41,6.39-10.92,7.22-12.8A12.92,12.92,0,0,0,18,8Z'/><path class='outline' d='M9.23,29.6a.57.57,0,0,1-.5-.35C7,27.57,2.24,18.09,1.48,16.38A13.57,13.57,0,0,1,.26,7.87C1.18,3.78,4,.92,7.7.23h0A8.38,8.38,0,0,1,11,.24h0c3.74.69,6.52,3.55,7.44,7.64a13.57,13.57,0,0,1-1.22,8.51c-.76,1.71-5.5,11.19-7.25,12.87a.57.57,0,0,1-.55.35H9.23ZM8,1,7.85,1a8.68,8.68,0,0,0-6.8,7C.5,10.52.86,13,2.22,16.05c.9,2,5.62,11.32,7.11,12.65,1.49-1.33,6.21-10.63,7.11-12.65,1.36-3.07,1.72-5.53,1.17-8h0a8.68,8.68,0,0,0-6.8-7l-.12,0A10.47,10.47,0,0,0,9.33.89,10.3,10.3,0,0,0,8,1Z'/><rect class='outline' x='3.12' y='6.34' width='12.53' height='7.76' rx='3.88'/><rect class='eyes' x='5.02' y='7.82' width='2.16' height='2.34' rx='1.02'/><rect class='eyes' x='11.25' y='7.82' width='2.16' height='2.34' rx='1.02'/><path class='eyes' d='M9.24,12.76A3.57,3.57,0,0,1,7,12a.4.4,0,1,1,.53-.61,2.78,2.78,0,0,0,3.49,0,.4.4,0,0,1,.48.65A3.71,3.71,0,0,1,9.24,12.76Z'/></svg>`;
|
||||
};
|
||||
|
||||
@ -45,13 +44,17 @@ const Map = ({
|
||||
interactive = false,
|
||||
}: Props): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { baseUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const [worldmap, setWorldmap] = useState<GeoJsonObject | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!worldmap) {
|
||||
getWorldmapGeojson(apiClient, baseUrl).then(setWorldmap);
|
||||
}
|
||||
getWorldmapGeojson(apiClient, hostUrl)
|
||||
.then((data) => {
|
||||
setWorldmap(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const RobotMarker = (
|
||||
@ -59,8 +62,8 @@ const Map = ({
|
||||
position: [number, number],
|
||||
orderType: number,
|
||||
order?: PublicOrder,
|
||||
) => {
|
||||
const fillColor = orderType == 1 ? theme.palette.primary.main : theme.palette.secondary.main;
|
||||
): JSX.Element => {
|
||||
const fillColor = orderType === 1 ? theme.palette.primary.main : theme.palette.secondary.main;
|
||||
const outlineColor = 'black';
|
||||
const eyesColor = 'white';
|
||||
|
||||
@ -77,10 +80,12 @@ const Map = ({
|
||||
})
|
||||
}
|
||||
eventHandlers={{
|
||||
click: (_event: LeafletMouseEvent) => order?.id && onOrderClicked(order.id),
|
||||
click: (_event: LeafletMouseEvent) => {
|
||||
order?.id && onOrderClicked(order.id);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{order && (
|
||||
{order != null && (
|
||||
<Tooltip direction='top'>
|
||||
<OrderTooltip order={order} />
|
||||
</Tooltip>
|
||||
@ -89,7 +94,7 @@ const Map = ({
|
||||
);
|
||||
};
|
||||
|
||||
const LocationMarker = () => {
|
||||
const LocationMarker = (): JSX.Element => {
|
||||
useMapEvents({
|
||||
click(event: LeafletMouseEvent) {
|
||||
if (interactive) {
|
||||
@ -98,16 +103,16 @@ const Map = ({
|
||||
},
|
||||
});
|
||||
|
||||
return position ? RobotMarker('marker', position, orderType || 0) : <></>;
|
||||
return position != null ? RobotMarker('marker', position, orderType ?? 0) : <></>;
|
||||
};
|
||||
|
||||
const getOrderMarkers = () => {
|
||||
const getOrderMarkers = (): JSX.Element => {
|
||||
if (orders.length < 1) return <></>;
|
||||
return (
|
||||
<MarkerClusterGroup showCoverageOnHover={true} disableClusteringAtZoom={14}>
|
||||
{orders.map((order) => {
|
||||
if (!order.latitude || !order.longitude) return <></>;
|
||||
return RobotMarker(order.id, [order.latitude, order.longitude], order.type || 0, order);
|
||||
if (!order?.latitude || !order?.longitude) return <></>;
|
||||
return RobotMarker(order.id, [order.latitude, order.longitude], order.type ?? 0, order);
|
||||
})}
|
||||
</MarkerClusterGroup>
|
||||
);
|
||||
@ -117,12 +122,12 @@ const Map = ({
|
||||
<MapContainer
|
||||
maxZoom={15}
|
||||
center={center ?? [0, 0]}
|
||||
zoom={zoom ? zoom : 2}
|
||||
zoom={zoom ?? 2}
|
||||
attributionControl={false}
|
||||
style={{ height: '100%', width: '100%', backgroundColor: theme.palette.background.paper }}
|
||||
>
|
||||
{!useTiles && !worldmap && <LinearProgress />}
|
||||
{!useTiles && worldmap && (
|
||||
{!useTiles && worldmap == null && <LinearProgress />}
|
||||
{!useTiles && worldmap != null && (
|
||||
<GeoJSON
|
||||
data={worldmap}
|
||||
style={{
|
||||
|
@ -1,21 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Tooltip,
|
||||
Alert,
|
||||
useTheme,
|
||||
IconButton,
|
||||
type TooltipProps,
|
||||
styled,
|
||||
tooltipClasses,
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type Order } from '../../models';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import { type Page } from '../../basic/NavBar';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface NotificationsProps {
|
||||
order: Order | undefined;
|
||||
rewards: number | undefined;
|
||||
page: Page;
|
||||
openProfile: () => void;
|
||||
@ -64,7 +62,6 @@ const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
}));
|
||||
|
||||
const Notifications = ({
|
||||
order,
|
||||
rewards,
|
||||
page,
|
||||
windowWidth,
|
||||
@ -72,6 +69,7 @@ const Notifications = ({
|
||||
}: NotificationsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage);
|
||||
const [inFocus, setInFocus] = useState<boolean>(true);
|
||||
@ -86,8 +84,8 @@ const Notifications = ({
|
||||
const position = windowWidth > 60 ? { top: '4em', right: '0em' } : { top: '0.5em', left: '50%' };
|
||||
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
|
||||
|
||||
const moveToOrderPage = function () {
|
||||
navigate(`/order/${order?.id}`);
|
||||
const moveToOrderPage = function (): void {
|
||||
navigate(`/order/${String(garage.getOrder()?.id)}`);
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
@ -108,7 +106,7 @@ const Notifications = ({
|
||||
|
||||
const Messages: MessagesProps = {
|
||||
bondLocked: {
|
||||
title: t(`${order?.is_maker ? 'Maker' : 'Taker'} bond locked`),
|
||||
title: t(`${garage.getOrder()?.is_maker === true ? 'Maker' : 'Taker'} bond locked`),
|
||||
severity: 'info',
|
||||
onClick: moveToOrderPage,
|
||||
sound: audio.ding,
|
||||
@ -208,28 +206,32 @@ const Notifications = ({
|
||||
},
|
||||
};
|
||||
|
||||
const notify = function (message: NotificationMessage) {
|
||||
if (message.title != '') {
|
||||
const notify = function (message: NotificationMessage): void {
|
||||
if (message.title !== '') {
|
||||
setMessage(message);
|
||||
setShow(true);
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
}, message.timeout);
|
||||
if (message.sound != null) {
|
||||
message.sound.play();
|
||||
void message.sound.play();
|
||||
}
|
||||
if (!inFocus) {
|
||||
setTitleAnimation(
|
||||
setInterval(function () {
|
||||
const title = document.title;
|
||||
document.title = title == basePageTitle ? message.pageTitle : basePageTitle;
|
||||
document.title = title === basePageTitle ? message.pageTitle : basePageTitle;
|
||||
}, 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = function (oldStatus: number | undefined, status: number) {
|
||||
const handleStatusChange = function (oldStatus: number | undefined, status: number): void {
|
||||
const order = garage.getOrder();
|
||||
|
||||
if (order === null) return;
|
||||
|
||||
let message = emptyNotificationMessage;
|
||||
|
||||
// Order status descriptions:
|
||||
@ -252,37 +254,35 @@ const Notifications = ({
|
||||
// 17: 'Maker lost dispute'
|
||||
// 18: 'Taker lost dispute'
|
||||
|
||||
if (status == 5 && oldStatus != 5) {
|
||||
if (status === 5 && oldStatus !== 5) {
|
||||
message = Messages.expired;
|
||||
} else if (oldStatus == undefined) {
|
||||
} else if (oldStatus === undefined) {
|
||||
message = emptyNotificationMessage;
|
||||
} else if (order?.is_maker && status > 0 && oldStatus == 0) {
|
||||
} else if (order.is_maker && status > 0 && oldStatus === 0) {
|
||||
message = Messages.bondLocked;
|
||||
} else if (order?.is_taker && status > 5 && oldStatus <= 5) {
|
||||
} else if (order.is_taker && status > 5 && oldStatus <= 5) {
|
||||
message = Messages.bondLocked;
|
||||
} else if (order?.is_maker && status > 5 && oldStatus <= 5) {
|
||||
} else if (order.is_maker && status > 5 && oldStatus <= 5) {
|
||||
message = Messages.taken;
|
||||
} else if (order?.is_seller && status > 7 && oldStatus < 7) {
|
||||
} else if (order.is_seller && status > 7 && oldStatus < 7) {
|
||||
message = Messages.escrowLocked;
|
||||
} else if ([9, 10].includes(status) && oldStatus < 9) {
|
||||
message = Messages.chat;
|
||||
} else if (order?.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
|
||||
} else if (order.is_seller && [13, 14, 15].includes(status) && oldStatus < 13) {
|
||||
message = Messages.successful;
|
||||
} else if (order?.is_buyer && status == 14 && oldStatus != 14) {
|
||||
} else if (order.is_buyer && status === 14 && oldStatus !== 14) {
|
||||
message = Messages.successful;
|
||||
} else if (order?.is_buyer && status == 15 && oldStatus < 14) {
|
||||
} else if (order.is_buyer && status === 15 && oldStatus < 14) {
|
||||
message = Messages.routingFailed;
|
||||
} else if (status == 11 && oldStatus < 11) {
|
||||
message = Messages.dispute;
|
||||
} else if (status == 11 && oldStatus < 11) {
|
||||
} else if (status === 11 && oldStatus < 11) {
|
||||
message = Messages.dispute;
|
||||
} else if (
|
||||
((order?.is_maker && status == 18) || (order?.is_taker && status == 17)) &&
|
||||
((order.is_maker && status === 18) || (order.is_taker && status === 17)) &&
|
||||
oldStatus < 17
|
||||
) {
|
||||
message = Messages.disputeWinner;
|
||||
} else if (
|
||||
((order?.is_maker && status == 17) || (order?.is_taker && status == 18)) &&
|
||||
((order.is_maker && status === 17) || (order.is_taker && status === 18)) &&
|
||||
oldStatus < 17
|
||||
) {
|
||||
message = Messages.disputeLoser;
|
||||
@ -293,20 +293,23 @@ const Notifications = ({
|
||||
|
||||
// Notify on order status change
|
||||
useEffect(() => {
|
||||
if (order != undefined && order.status != oldOrderStatus) {
|
||||
const order = garage.getOrder();
|
||||
if (order !== null) {
|
||||
if (order.status !== oldOrderStatus) {
|
||||
handleStatusChange(oldOrderStatus, order.status);
|
||||
setOldOrderStatus(order.status);
|
||||
} else if (order != undefined && order.chat_last_index > oldChatIndex) {
|
||||
if (page != 'order') {
|
||||
} else if (order.chat_last_index > oldChatIndex) {
|
||||
if (page !== 'order') {
|
||||
notify(Messages.chatMessage);
|
||||
}
|
||||
setOldChatIndex(order.chat_last_index);
|
||||
}
|
||||
}, [order]);
|
||||
}
|
||||
}, [orderUpdatedAt]);
|
||||
|
||||
// Notify on rewards change
|
||||
useEffect(() => {
|
||||
if (rewards != undefined) {
|
||||
if (rewards !== undefined) {
|
||||
if (rewards > oldRewards) {
|
||||
notify(Messages.rewards);
|
||||
}
|
||||
@ -316,7 +319,7 @@ const Notifications = ({
|
||||
|
||||
// Set blinking page title and clear on visibility change > infocus
|
||||
useEffect(() => {
|
||||
if (titleAnimation != undefined && inFocus) {
|
||||
if (titleAnimation !== undefined && inFocus) {
|
||||
clearInterval(titleAnimation);
|
||||
}
|
||||
}, [inFocus]);
|
||||
|
@ -7,9 +7,9 @@ interface Props {
|
||||
totalSecsExp: number;
|
||||
}
|
||||
|
||||
const LinearDeterminate = ({ expiresAt, totalSecsExp }: Props): JSX.Element => {
|
||||
const timePercentLeft = function () {
|
||||
if (expiresAt && totalSecsExp) {
|
||||
const LinearDeterminate: React.FC<Props> = ({ expiresAt, totalSecsExp }) => {
|
||||
const timePercentLeft = function (): number {
|
||||
if (Boolean(expiresAt) && Boolean(totalSecsExp)) {
|
||||
const lapseTime = calcTimeDelta(new Date(expiresAt)).total / 1000;
|
||||
return (lapseTime / totalSecsExp) * 100;
|
||||
} else {
|
||||
|
@ -25,13 +25,13 @@ import { type Order, type Info } from '../../models';
|
||||
import { ConfirmationDialog } from '../Dialogs';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { computeSats } from '../../utils';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface TakeButtonProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
baseUrl: string;
|
||||
info: Info;
|
||||
info?: Info;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
@ -42,15 +42,15 @@ interface OpenDialogsProps {
|
||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
||||
|
||||
const TakeButton = ({
|
||||
order,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
info,
|
||||
onClickGenerateRobot = () => null,
|
||||
}: TakeButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { robot } = useContext<UseAppStoreType>(AppContext);
|
||||
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||
const [badRequest, setBadRequest] = useState<string>('');
|
||||
@ -58,11 +58,15 @@ const TakeButton = ({
|
||||
const [open, setOpen] = useState<OpenDialogsProps>(closeAll);
|
||||
const [satoshis, setSatoshis] = useState<string>('');
|
||||
|
||||
const satoshisNow = () => {
|
||||
const satoshisNow = (): string | undefined => {
|
||||
const order = garage.getOrder();
|
||||
|
||||
if (order === null) return;
|
||||
|
||||
const tradeFee = info?.taker_fee ?? 0;
|
||||
const defaultRoutingBudget = 0.001;
|
||||
const btc_now = order.satoshis_now / 100000000;
|
||||
const rate = order.amount ? order.amount / btc_now : order.max_amount / btc_now;
|
||||
const btcNow = order.satoshis_now / 100000000;
|
||||
const rate = order.amount != null ? order.amount / btcNow : order.max_amount / btcNow;
|
||||
const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
const satoshis = computeSats({
|
||||
amount,
|
||||
@ -74,12 +78,13 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSatoshis(satoshisNow());
|
||||
}, [order.satoshis_now, takeAmount, info]);
|
||||
setSatoshis(satoshisNow() ?? '');
|
||||
}, [orderUpdatedAt, takeAmount, info]);
|
||||
|
||||
const currencyCode: string = order.currency == 1000 ? 'Sats' : currencies[`${order.currency}`];
|
||||
const currencyCode: string =
|
||||
garage.getOrder()?.currency === 1000 ? 'Sats' : currencies[`${garage.getOrder()?.currency}`];
|
||||
|
||||
const InactiveMakerDialog = function () {
|
||||
const InactiveMakerDialog = function (): JSX.Element {
|
||||
return (
|
||||
<Dialog
|
||||
open={open.inactiveMaker}
|
||||
@ -116,7 +121,15 @@ const TakeButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
const countdownTakeOrderRenderer = function ({ seconds, completed }) {
|
||||
interface countdownTakeOrderRendererProps {
|
||||
seconds: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
const countdownTakeOrderRenderer = function ({
|
||||
seconds,
|
||||
completed,
|
||||
}: countdownTakeOrderRendererProps): JSX.Element {
|
||||
if (isNaN(seconds) || completed) {
|
||||
return takeOrderButton();
|
||||
} else {
|
||||
@ -132,8 +145,8 @@ const TakeButton = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleTakeAmountChange = function (e) {
|
||||
if (e.target.value != '' && e.target.value != null) {
|
||||
const handleTakeAmountChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
if (e.target.value !== '' && e.target.value != null) {
|
||||
setTakeAmount(`${parseFloat(e.target.value)}`);
|
||||
} else {
|
||||
setTakeAmount(e.target.value);
|
||||
@ -141,18 +154,22 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
const amountHelperText = useMemo(() => {
|
||||
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
if (amount < Number(order.min_amount) && takeAmount != '') {
|
||||
const order = garage.getOrder();
|
||||
|
||||
if (order === null) return;
|
||||
|
||||
const amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
if (amount < Number(order.min_amount) && takeAmount !== '') {
|
||||
return t('Too low');
|
||||
} else if (amount > Number(order.max_amount) && takeAmount != '') {
|
||||
} else if (amount > Number(order.max_amount) && takeAmount !== '') {
|
||||
return t('Too high');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [order, takeAmount]);
|
||||
}, [orderUpdatedAt, takeAmount]);
|
||||
|
||||
const onTakeOrderClicked = function () {
|
||||
if (order.maker_status == 'Inactive') {
|
||||
const onTakeOrderClicked = function (): void {
|
||||
if (garage.getOrder()?.maker_status === 'Inactive') {
|
||||
setOpen({ inactiveMaker: true, confirmation: false });
|
||||
} else {
|
||||
setOpen({ inactiveMaker: false, confirmation: true });
|
||||
@ -160,17 +177,18 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
const invalidTakeAmount = useMemo(() => {
|
||||
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
const order = garage.getOrder();
|
||||
const amount = order?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
return (
|
||||
amount < Number(order.min_amount) ||
|
||||
amount > Number(order.max_amount) ||
|
||||
takeAmount == '' ||
|
||||
amount < Number(order?.min_amount) ||
|
||||
amount > Number(order?.max_amount) ||
|
||||
takeAmount === '' ||
|
||||
takeAmount == null
|
||||
);
|
||||
}, [takeAmount, order]);
|
||||
}, [takeAmount, orderUpdatedAt]);
|
||||
|
||||
const takeOrderButton = function () {
|
||||
if (order.has_range) {
|
||||
const takeOrderButton = function (): JSX.Element {
|
||||
if (garage.getOrder()?.has_range) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -209,8 +227,8 @@ const TakeButton = ({
|
||||
required={true}
|
||||
value={takeAmount}
|
||||
inputProps={{
|
||||
min: order.min_amount,
|
||||
max: order.max_amount,
|
||||
min: garage.getOrder()?.min_amount,
|
||||
max: garage.getOrder()?.max_amount,
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={handleTakeAmountChange}
|
||||
@ -260,10 +278,10 @@ const TakeButton = ({
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{satoshis != '0' && satoshis != '' && !invalidTakeAmount ? (
|
||||
{satoshis !== '0' && satoshis !== '' && !invalidTakeAmount ? (
|
||||
<Grid item>
|
||||
<FormHelperText sx={{ position: 'relative', top: '0.15em' }}>
|
||||
{order.type === 1
|
||||
{garage.getOrder()?.type === 1
|
||||
? t('You will receive {{satoshis}} Sats (Approx)', { satoshis })
|
||||
: t('You will send {{satoshis}} Sats (Approx)', { satoshis })}
|
||||
</FormHelperText>
|
||||
@ -296,33 +314,44 @@ const TakeButton = ({
|
||||
}
|
||||
};
|
||||
|
||||
const takeOrder = function () {
|
||||
const takeOrder = function (): void {
|
||||
if (!focusedCoordinator) return;
|
||||
|
||||
setLoadingTake(true);
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/order/?order_id=' + order.id,
|
||||
url,
|
||||
`/api/order/?order_id=${String(garage.getOrder()?.id)}`,
|
||||
{
|
||||
action: 'take',
|
||||
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
amount: garage.getOrder()?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
},
|
||||
{ tokenSHA256: robot.tokenSHA256 },
|
||||
{ tokenSHA256: garage.getRobot().tokenSHA256 },
|
||||
)
|
||||
.then((data) => {
|
||||
setLoadingTake(false);
|
||||
if (data.bad_request) {
|
||||
if (data?.bad_request !== undefined) {
|
||||
setBadRequest(data.bad_request);
|
||||
} else {
|
||||
setOrder(data);
|
||||
garage.updateOrder(data as Order);
|
||||
setBadRequest('');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setBadRequest('Request error');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Countdown date={new Date(order.penalty)} renderer={countdownTakeOrderRenderer} />
|
||||
{badRequest != '' ? (
|
||||
<Countdown
|
||||
date={new Date(garage.getOrder()?.penalty ?? '')}
|
||||
renderer={countdownTakeOrderRenderer}
|
||||
/>
|
||||
{badRequest !== '' ? (
|
||||
<Box style={{ padding: '0.5em' }}>
|
||||
<Typography align='center' color='secondary'>
|
||||
{t(badRequest)}
|
||||
@ -342,7 +371,7 @@ const TakeButton = ({
|
||||
setLoadingTake(true);
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
hasRobot={garage.getRobot().avatarLoaded}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
<InactiveMakerDialog />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
List,
|
||||
@ -15,7 +15,7 @@ import {
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
ListItemButton,
|
||||
} from '@mui/material';
|
||||
|
||||
import Countdown, { type CountdownRenderProps, zeroPad } from 'react-countdown';
|
||||
@ -36,42 +36,48 @@ import { PaymentStringAsIcons } from '../../components/PaymentMethods';
|
||||
import { FlagWithProps, SendReceiveIcon } from '../Icons';
|
||||
import LinearDeterminate from './LinearDeterminate';
|
||||
|
||||
import { type Order, type Info } from '../../models';
|
||||
import type { Order, Coordinator } from '../../models';
|
||||
import { statusBadgeColor, pn, amountToString, computeSats } from '../../utils';
|
||||
import TakeButton from './TakeButton';
|
||||
import { F2fMapDialog } from '../Dialogs';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface OrderDetailsProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
info: Info;
|
||||
coordinator: Coordinator;
|
||||
onClickCoordinator?: () => void;
|
||||
baseUrl: string;
|
||||
hasRobot: boolean;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
const OrderDetails = ({
|
||||
order,
|
||||
info,
|
||||
setOrder,
|
||||
coordinator,
|
||||
onClickCoordinator = () => null,
|
||||
baseUrl,
|
||||
hasRobot,
|
||||
onClickGenerateRobot = () => null,
|
||||
}: OrderDetailsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const theme = useTheme();
|
||||
const { orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { currentOrder } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
const currencyCode: string = currencies[(currentOrder.order?.currency ?? 1).toString()];
|
||||
const [showSatsDetails, setShowSatsDetails] = useState<boolean>(false);
|
||||
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
|
||||
|
||||
const amountString = useMemo(() => {
|
||||
// precision to 8 decimal if currency is BTC otherwise 4 decimals
|
||||
if (order.currency == 1000) {
|
||||
const order = currentOrder.order;
|
||||
|
||||
if (order === null) return;
|
||||
|
||||
if (order.currency === 1000) {
|
||||
return (
|
||||
amountToString(
|
||||
order.amount * 100000000,
|
||||
order.amount ? false : order.has_range,
|
||||
(order.amount * 100000000).toString(),
|
||||
order.amount > 0 ? false : order.has_range,
|
||||
order.min_amount * 100000000,
|
||||
order.max_amount * 100000000,
|
||||
) + ' Sats'
|
||||
@ -79,14 +85,14 @@ const OrderDetails = ({
|
||||
} else {
|
||||
return (
|
||||
amountToString(
|
||||
order.amount,
|
||||
order.amount ? false : order.has_range,
|
||||
order.amount.toString(),
|
||||
order.amount > 0 ? false : order.has_range,
|
||||
order.min_amount,
|
||||
order.max_amount,
|
||||
) + ` ${currencyCode}`
|
||||
);
|
||||
}
|
||||
}, [order.currency, order.amount, order.min_amount, order.max_amount, order.has_range]);
|
||||
}, [orderUpdatedAt]);
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
const countdownRenderer = function ({
|
||||
@ -95,13 +101,13 @@ const OrderDetails = ({
|
||||
minutes,
|
||||
seconds,
|
||||
completed,
|
||||
}: CountdownRenderProps) {
|
||||
}: CountdownRenderProps): JSX.Element {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return <span> {t('The order has expired')}</span>;
|
||||
} else {
|
||||
let color = 'inherit';
|
||||
const fraction_left = total / 1000 / order.total_secs_exp;
|
||||
const fraction_left = total / 1000 / (currentOrder.order?.total_secs_exp ?? 1);
|
||||
// Make orange at 25% of time left
|
||||
if (fraction_left < 0.25) {
|
||||
color = theme.palette.warning.main;
|
||||
@ -121,18 +127,26 @@ const OrderDetails = ({
|
||||
}
|
||||
};
|
||||
|
||||
const timerRenderer = function (seconds: number) {
|
||||
const timerRenderer = function (seconds: number): JSX.Element {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds - hours * 3600) / 60);
|
||||
return (
|
||||
<span>
|
||||
{hours > 0 ? hours + 'h' : ''} {minutes > 0 ? zeroPad(minutes) + 'm' : ''}{' '}
|
||||
{hours > 0 ? `${hours}h` : ''} {minutes > 0 ? `${zeroPad(minutes)}m` : ''}{' '}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
const countdownPenaltyRenderer = function ({ minutes, seconds, completed }) {
|
||||
const countdownPenaltyRenderer = ({
|
||||
minutes,
|
||||
seconds,
|
||||
completed,
|
||||
}: {
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
completed: boolean;
|
||||
}): JSX.Element => {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return <span> {t('Penalty lifted, good to go!')}</span>;
|
||||
@ -153,15 +167,20 @@ const OrderDetails = ({
|
||||
let send: string = '';
|
||||
let receive: string = '';
|
||||
let sats: string = '';
|
||||
const order = currentOrder.order;
|
||||
|
||||
const isBuyer = (order.type == 0 && order.is_maker) || (order.type == 1 && !order.is_maker);
|
||||
const tradeFee = order.is_maker ? info?.maker_fee ?? 0 : info?.taker_fee ?? 0;
|
||||
if (order === null) return {};
|
||||
|
||||
const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker);
|
||||
const tradeFee = order.is_maker
|
||||
? coordinator.info?.maker_fee ?? 0
|
||||
: coordinator.info?.taker_fee ?? 0;
|
||||
const defaultRoutingBudget = 0.001;
|
||||
const btc_now = order.satoshis_now / 100000000;
|
||||
const rate = order.amount ? order.amount / btc_now : order.max_amount / btc_now;
|
||||
const rate = order.amount > 0 ? order.amount / btc_now : Number(order.max_amount) / btc_now;
|
||||
|
||||
if (isBuyer) {
|
||||
if (order.amount) {
|
||||
if (order.amount > 0) {
|
||||
sats = computeSats({
|
||||
amount: order.amount,
|
||||
fee: -tradeFee,
|
||||
@ -181,7 +200,7 @@ const OrderDetails = ({
|
||||
routingBudget: defaultRoutingBudget,
|
||||
rate,
|
||||
});
|
||||
sats = `${min}-${max}`;
|
||||
sats = `${String(min)}-${String(max)}`;
|
||||
}
|
||||
send = t('You send via {{method}} {{amount}}', {
|
||||
amount: amountString,
|
||||
@ -192,7 +211,7 @@ const OrderDetails = ({
|
||||
amount: sats,
|
||||
});
|
||||
} else {
|
||||
if (order.amount) {
|
||||
if (order.amount > 0) {
|
||||
sats = computeSats({
|
||||
amount: order.amount,
|
||||
fee: tradeFee,
|
||||
@ -209,7 +228,7 @@ const OrderDetails = ({
|
||||
fee: tradeFee,
|
||||
rate,
|
||||
});
|
||||
sats = `${min}-${max}`;
|
||||
sats = `${String(min)}-${String(max)}`;
|
||||
}
|
||||
send = t('You send via Lightning {{amount}} Sats (Approx)', { amount: sats });
|
||||
receive = t('You receive via {{method}} {{amount}}', {
|
||||
@ -218,62 +237,93 @@ const OrderDetails = ({
|
||||
});
|
||||
}
|
||||
return { send, receive };
|
||||
}, [order.currency, order.satoshis_now, order.amount, order.has_range]);
|
||||
}, [orderUpdatedAt]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
<F2fMapDialog
|
||||
latitude={order.latitude}
|
||||
longitude={order.longitude}
|
||||
latitude={currentOrder.order?.latitude}
|
||||
longitude={currentOrder.order?.longitude}
|
||||
open={openWorldmap}
|
||||
orderType={order.type || 0}
|
||||
orderType={currentOrder.order?.type ?? 0}
|
||||
zoom={6}
|
||||
message={t(
|
||||
'The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.',
|
||||
)}
|
||||
onClose={() => setOpenWorldmap(false)}
|
||||
onClose={() => {
|
||||
setOpenWorldmap(false);
|
||||
}}
|
||||
/>
|
||||
<Grid item xs={12}>
|
||||
<List dense={true}>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
onClickCoordinator();
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Grid container direction='row' justifyContent='center' alignItems='center'>
|
||||
<Grid item xs={2}>
|
||||
<RobotAvatar
|
||||
nickname={coordinator.shortAlias}
|
||||
coordinator={true}
|
||||
baseUrl={hostUrl}
|
||||
small={true}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ListItemText primary={coordinator.longAlias} secondary={t('Order host')} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItemButton>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar sx={{ width: '4em', height: '4em' }}>
|
||||
<RobotAvatar
|
||||
statusColor={statusBadgeColor(order.maker_status)}
|
||||
nickname={order.maker_nick}
|
||||
tooltip={t(order.maker_status)}
|
||||
orderType={order.type}
|
||||
statusColor={statusBadgeColor(currentOrder.order?.maker_status ?? '')}
|
||||
nickname={currentOrder.order?.maker_nick}
|
||||
tooltip={t(currentOrder.order?.maker_status ?? '')}
|
||||
orderType={currentOrder.order?.type}
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={`${order.maker_nick} (${
|
||||
order.type
|
||||
? t(order.currency == 1000 ? 'Swapping Out' : 'Seller')
|
||||
: t(order.currency == 1000 ? 'Swapping In' : 'Buyer')
|
||||
primary={`${currentOrder.order?.maker_nick} (${
|
||||
currentOrder.order?.type === 1
|
||||
? t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
|
||||
: t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
|
||||
})`}
|
||||
secondary={t('Order maker')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={order.is_participant && order.taker_nick !== 'None'}>
|
||||
<Collapse
|
||||
in={currentOrder.order?.is_participant && currentOrder.order?.taker_nick !== 'None'}
|
||||
>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`${order.taker_nick} (${
|
||||
order.type
|
||||
? t(order.currency == 1000 ? 'Swapping In' : 'Buyer')
|
||||
: t(order.currency == 1000 ? 'Swapping Out' : 'Seller')
|
||||
primary={`${currentOrder.order?.taker_nick} (${
|
||||
currentOrder.order?.type === 1
|
||||
? t(currentOrder.order?.currency === 1000 ? 'Swapping In' : 'Buyer')
|
||||
: t(currentOrder.order?.currency === 1000 ? 'Swapping Out' : 'Seller')
|
||||
})`}
|
||||
secondary={t('Order taker')}
|
||||
/>
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
avatarClass='smallAvatar'
|
||||
statusColor={statusBadgeColor(order.taker_status)}
|
||||
nickname={order.taker_nick == 'None' ? undefined : order.taker_nick}
|
||||
tooltip={t(order.taker_status)}
|
||||
orderType={order.type === 0 ? 1 : 0}
|
||||
statusColor={statusBadgeColor(currentOrder.order?.taker_status ?? '')}
|
||||
nickname={
|
||||
currentOrder.order?.taker_nick === 'None'
|
||||
? undefined
|
||||
: currentOrder.order?.taker_nick
|
||||
}
|
||||
tooltip={t(currentOrder.order?.taker_status ?? '')}
|
||||
orderType={currentOrder.order?.type === 0 ? 1 : 0}
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
/>
|
||||
@ -284,12 +334,15 @@ const OrderDetails = ({
|
||||
<Chip label={t('Order Details')} />
|
||||
</Divider>
|
||||
|
||||
<Collapse in={order.is_participant}>
|
||||
<Collapse in={currentOrder.order?.is_participant}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(order.status_message)} secondary={t('Order status')} />
|
||||
<ListItemText
|
||||
primary={t(currentOrder.order?.status_message ?? '')}
|
||||
secondary={t('Order status')}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Collapse>
|
||||
@ -311,7 +364,7 @@ const OrderDetails = ({
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={amountString}
|
||||
secondary={order.amount ? 'Amount' : 'Amount Range'}
|
||||
secondary={(currentOrder.order?.amount ?? 0) > 0 ? 'Amount' : 'Amount Range'}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<IconButton
|
||||
@ -360,18 +413,24 @@ const OrderDetails = ({
|
||||
size={1.42 * theme.typography.fontSize}
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
text={order.payment_method}
|
||||
text={currentOrder.order?.payment_method}
|
||||
/>
|
||||
}
|
||||
secondary={
|
||||
order.currency == 1000 ? t('Swap destination') : t('Accepted payment methods')
|
||||
currentOrder.order?.currency === 1000
|
||||
? t('Swap destination')
|
||||
: t('Accepted payment methods')
|
||||
}
|
||||
/>
|
||||
{order.payment_method.includes('Cash F2F') && (
|
||||
{currentOrder.order?.payment_method.includes('Cash F2F') && (
|
||||
<ListItemIcon>
|
||||
<Tooltip enterTouchDelay={0} title={t('F2F location')}>
|
||||
<div>
|
||||
<IconButton onClick={() => setOpenWorldmap(true)}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpenWorldmap(true);
|
||||
}}
|
||||
>
|
||||
<Map />
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -387,24 +446,27 @@ const OrderDetails = ({
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
|
||||
{order.price_now !== undefined ? (
|
||||
{currentOrder.order?.price_now !== undefined ? (
|
||||
<ListItemText
|
||||
primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', {
|
||||
price: pn(order.price_now),
|
||||
price: pn(currentOrder.order?.price_now),
|
||||
currencyCode,
|
||||
premium: order.premium_now,
|
||||
premium: currentOrder.order?.premium_now,
|
||||
})}
|
||||
secondary={t('Price and Premium')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!order.price_now && order.is_explicit ? (
|
||||
<ListItemText primary={pn(order.satoshis)} secondary={t('Amount of Satoshis')} />
|
||||
{currentOrder.order?.price_now === undefined && currentOrder.order?.is_explicit ? (
|
||||
<ListItemText
|
||||
primary={pn(currentOrder.order?.satoshis)}
|
||||
secondary={t('Amount of Satoshis')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!order.price_now && !order.is_explicit ? (
|
||||
{currentOrder.order?.price_now === undefined && !currentOrder.order?.is_explicit ? (
|
||||
<ListItemText
|
||||
primary={parseFloat(Number(order.premium).toFixed(2)) + '%'}
|
||||
primary={`${parseFloat(Number(currentOrder.order?.premium).toFixed(2))}%`}
|
||||
secondary={t('Premium over market price')}
|
||||
/>
|
||||
) : null}
|
||||
@ -418,7 +480,7 @@ const OrderDetails = ({
|
||||
</ListItemIcon>
|
||||
<Grid container>
|
||||
<Grid item xs={4.5}>
|
||||
<ListItemText primary={order.id} secondary={t('Order ID')} />
|
||||
<ListItemText primary={currentOrder.order?.id} secondary={t('Order ID')} />
|
||||
</Grid>
|
||||
<Grid item xs={7.5}>
|
||||
<Grid container>
|
||||
@ -429,7 +491,7 @@ const OrderDetails = ({
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<ListItemText
|
||||
primary={timerRenderer(order.escrow_duration)}
|
||||
primary={timerRenderer(currentOrder.order?.escrow_duration)}
|
||||
secondary={t('Deposit timer')}
|
||||
></ListItemText>
|
||||
</Grid>
|
||||
@ -439,39 +501,47 @@ const OrderDetails = ({
|
||||
</ListItem>
|
||||
|
||||
{/* if order is in a status that does not expire, do not show countdown */}
|
||||
<Collapse in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(order.status)}>
|
||||
<Collapse
|
||||
in={![4, 5, 12, 13, 14, 15, 16, 17, 18].includes(currentOrder.order?.status ?? 0)}
|
||||
>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<AccessTime />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={t('Expires in')}>
|
||||
<Countdown date={new Date(order.expires_at)} renderer={countdownRenderer} />
|
||||
<Countdown
|
||||
date={new Date(currentOrder.order?.expires_at ?? '')}
|
||||
renderer={countdownRenderer}
|
||||
/>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<LinearDeterminate totalSecsExp={order.total_secs_exp} expiresAt={order.expires_at} />
|
||||
<LinearDeterminate
|
||||
totalSecsExp={currentOrder.order?.total_secs_exp ?? 0}
|
||||
expiresAt={currentOrder.order?.expires_at ?? ''}
|
||||
/>
|
||||
</Collapse>
|
||||
</List>
|
||||
|
||||
{/* If the user has a penalty/limit */}
|
||||
{order.penalty !== undefined ? (
|
||||
{currentOrder.order?.penalty !== undefined ? (
|
||||
<Grid item xs={12}>
|
||||
<Alert severity='warning' sx={{ borderRadius: '0' }}>
|
||||
<Countdown date={new Date(order.penalty)} renderer={countdownPenaltyRenderer} />
|
||||
<Countdown
|
||||
date={new Date(currentOrder.order?.penalty ?? '')}
|
||||
renderer={countdownPenaltyRenderer}
|
||||
/>
|
||||
</Alert>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!order.is_participant ? (
|
||||
{!currentOrder.order?.is_participant ? (
|
||||
<Grid item xs={12}>
|
||||
<TakeButton
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
hasRobot={hasRobot}
|
||||
info={info}
|
||||
info={coordinator.info}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
</Grid>
|
||||
|
@ -19,11 +19,11 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
|
||||
|
||||
const parsedText = useMemo(() => {
|
||||
const rows = [];
|
||||
let custom_methods = text;
|
||||
let customMethods = text;
|
||||
// Adds icons for each PaymentMethod that matches
|
||||
methods.forEach((method, i) => {
|
||||
if (text.includes(method.name)) {
|
||||
custom_methods = custom_methods.replace(method.name, '');
|
||||
customMethods = customMethods.replace(method.name, '');
|
||||
rows.push(
|
||||
<Tooltip
|
||||
key={`${method.name}-${i}`}
|
||||
@ -46,20 +46,20 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
|
||||
});
|
||||
|
||||
// Adds a Custom icon if there are words that do not match
|
||||
const chars_left = custom_methods
|
||||
const charsLeft = customMethods
|
||||
.replace(' ', '')
|
||||
.replace(' ', '')
|
||||
.replace(' ', '')
|
||||
.replace(' ', '')
|
||||
.replace(' ', '');
|
||||
|
||||
if (chars_left.length > 0) {
|
||||
if (charsLeft.length > 0) {
|
||||
rows.push(
|
||||
<Tooltip
|
||||
key={'pushed'}
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
title={verbose ? othersText : othersText + ': ' + custom_methods}
|
||||
title={verbose ? othersText : othersText + ': ' + customMethods}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@ -82,7 +82,7 @@ const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props
|
||||
{rows}{' '}
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
{' '}
|
||||
<span>{custom_methods}</span>
|
||||
<span>{customMethods}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import SmoothImage from 'react-smooth-image';
|
||||
import { Avatar, Badge, Tooltip, useTheme } from '@mui/material';
|
||||
import { Avatar, Badge, Tooltip } from '@mui/material';
|
||||
import { SendReceiveIcon } from '../Icons';
|
||||
import { apiClient } from '../../services/api';
|
||||
import placeholder from './placeholder.json';
|
||||
import { UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface Props {
|
||||
nickname: string | undefined;
|
||||
smooth?: boolean;
|
||||
coordinator?: boolean;
|
||||
small?: boolean;
|
||||
flipHorizontally?: boolean;
|
||||
style?: object;
|
||||
@ -41,27 +44,35 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
avatarClass = 'flippedSmallAvatar',
|
||||
imageStyle = {},
|
||||
onLoad = () => {},
|
||||
coordinator = false,
|
||||
baseUrl,
|
||||
}) => {
|
||||
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>();
|
||||
const [nicknameReady, setNicknameReady] = useState<boolean>(false);
|
||||
const [activeBackground, setActiveBackground] = useState<boolean>(true);
|
||||
|
||||
const path = coordinator ? '/static/federation/' : '/static/assets/avatars/';
|
||||
const [backgroundData] = useState<BackgroundData>(
|
||||
placeholderType == 'generating' ? placeholder.generating : placeholder.loading,
|
||||
placeholderType === 'generating' ? placeholder.generating : placeholder.loading,
|
||||
);
|
||||
const backgroundImage = `url(data:${backgroundData.mime};base64,${backgroundData.data})`;
|
||||
const className = placeholderType == 'loading' ? 'loadingAvatar' : 'generatingAvatar';
|
||||
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
|
||||
|
||||
useEffect(() => {
|
||||
if (nickname != undefined) {
|
||||
if (nickname !== undefined) {
|
||||
if (window.NativeRobosats === undefined) {
|
||||
setAvatarSrc(`${baseUrl}/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`);
|
||||
setAvatarSrc(`${baseUrl}${path}${nickname}${small ? '.small' : ''}.webp`);
|
||||
setNicknameReady(true);
|
||||
} else {
|
||||
} else if (focusedCoordinator) {
|
||||
setNicknameReady(true);
|
||||
apiClient
|
||||
.fileImageUrl(baseUrl, `/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`)
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
void apiClient
|
||||
.fileImageUrl(url, `${path}${nickname}${small ? '.small' : ''}.webp`)
|
||||
.then(setAvatarSrc);
|
||||
}
|
||||
} else {
|
||||
@ -133,7 +144,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
const getAvatarWithBadges = useCallback(() => {
|
||||
let component = avatar;
|
||||
|
||||
if (statusColor) {
|
||||
if (statusColor !== undefined) {
|
||||
component = (
|
||||
<Badge variant='dot' overlap='circular' badgeContent='' color={statusColor}>
|
||||
{component}
|
||||
@ -153,7 +164,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
if (tooltip !== undefined) {
|
||||
component = (
|
||||
<Tooltip placement={tooltipPosition} enterTouchDelay={0} title={tooltip}>
|
||||
{component}
|
||||
|
337
frontend/src/components/RobotInfo/index.tsx
Normal file
337
frontend/src/components/RobotInfo/index.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Grid,
|
||||
useTheme,
|
||||
Divider,
|
||||
Typography,
|
||||
Badge,
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from '@mui/material';
|
||||
import { Numbers, Send, EmojiEvents, ExpandMore } from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type Coordinator, type Robot } from '../../models';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EnableTelegramDialog } from '../Dialogs';
|
||||
import { UserNinjaIcon } from '../Icons';
|
||||
|
||||
import { getWebln } from '../../utils';
|
||||
import { signCleartextMessage } from '../../pgp';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface Props {
|
||||
robot: Robot;
|
||||
slotIndex: number;
|
||||
coordinator: Coordinator;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }: Props) => {
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [rewardInvoice, setRewardInvoice] = useState<string>('');
|
||||
const [showRewardsSpinner, setShowRewardsSpinner] = useState<boolean>(false);
|
||||
const [withdrawn, setWithdrawn] = useState<boolean>(false);
|
||||
const [badInvoice, setBadInvoice] = useState<string>('');
|
||||
const [openClaimRewards, setOpenClaimRewards] = useState<boolean>(false);
|
||||
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
|
||||
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
|
||||
|
||||
const handleWebln = async (): Promise<void> => {
|
||||
void getWebln()
|
||||
.then(() => {
|
||||
setWeblnEnabled(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setWeblnEnabled(false);
|
||||
console.log('WebLN not available');
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleWebln();
|
||||
}, []);
|
||||
|
||||
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
if (robot.earnedRewards > 0) {
|
||||
const webln = await getWebln();
|
||||
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
|
||||
if (invoice != null) {
|
||||
handleSubmitInvoiceClicked(e, invoice.paymentRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitInvoiceClicked = (e: any, rewardInvoice: string): void => {
|
||||
setBadInvoice('');
|
||||
setShowRewardsSpinner(true);
|
||||
|
||||
const robot = garage.getRobot(slotIndex);
|
||||
|
||||
if (robot.encPrivKey && robot.token) {
|
||||
void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then(
|
||||
(signedInvoice) => {
|
||||
coordinator.fetchReward(signedInvoice, garage, slotIndex).then((data) => {
|
||||
setBadInvoice(data.bad_invoice ?? '');
|
||||
setShowRewardsSpinner(false);
|
||||
setWithdrawn(data.successful_withdrawal);
|
||||
setOpenClaimRewards(!(data.successful_withdrawal !== undefined));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const setStealthInvoice = (wantsStealth: boolean): void => {
|
||||
coordinator.fetchStealth(wantsStealth, garage, slotIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
{`${coordinator.longAlias}:`}
|
||||
{garage.getRobot(slotIndex).earnedRewards > 0 && (
|
||||
<Typography color='success'> {t('Claim Sats!')} </Typography>
|
||||
)}
|
||||
{(garage.getRobot(slotIndex).activeOrderId ?? 0) > 0 && (
|
||||
<Typography color='success'>
|
||||
<b>{t('Active order!')}</b>
|
||||
</Typography>
|
||||
)}
|
||||
{(garage.getRobot(slotIndex).lastOrderId ?? 0) > 0 && robot.activeOrderId === undefined && (
|
||||
<Typography color='warning'> {t('finished order')}</Typography>
|
||||
)}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List dense disablePadding={true}>
|
||||
{(garage.getRobot(slotIndex).activeOrderId ?? 0) > 0 ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(`/order/${coordinator.shortAlias}/${String(robot.activeOrderId)}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent='' color='primary'>
|
||||
<Numbers color='primary' />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
secondary={t('Your current order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : (garage.getRobot(slotIndex).lastOrderId ?? 0) > 0 ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
navigate(`/order/${coordinator.shortAlias}/${String(robot.lastOrderId)}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Numbers color='primary' />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
secondary={t('Inactive order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Numbers />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('No active orders')}
|
||||
secondary={t('You do not have previous orders')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<EnableTelegramDialog
|
||||
open={openEnableTelegram}
|
||||
onClose={() => {
|
||||
setOpenEnableTelegram(false);
|
||||
}}
|
||||
tgBotName={robot.tgBotName}
|
||||
tgToken={robot.tgToken}
|
||||
/>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Send />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
{robot.tgEnabled ? (
|
||||
<Typography color={theme.palette.success.main}>
|
||||
<b>{t('Telegram enabled')}</b>
|
||||
</Typography>
|
||||
) : (
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
setOpenEnableTelegram(true);
|
||||
}}
|
||||
>
|
||||
{t('Enable Telegram Notifications')}
|
||||
</Button>
|
||||
)}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<UserNinjaIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText>
|
||||
<Tooltip
|
||||
placement='bottom'
|
||||
enterTouchDelay={0}
|
||||
title={t(
|
||||
"Stealth lightning invoices do not contain details about the trade except an order reference. Enable this setting if you don't want to disclose details to a custodial lightning wallet.",
|
||||
)}
|
||||
>
|
||||
<Grid item>
|
||||
<FormControlLabel
|
||||
labelPlacement='end'
|
||||
label={t('Use stealth invoices')}
|
||||
control={
|
||||
<Switch
|
||||
checked={robot.stealthInvoices}
|
||||
onChange={() => {
|
||||
setStealthInvoice(!robot.stealthInvoices);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EmojiEvents />
|
||||
</ListItemIcon>
|
||||
|
||||
{!openClaimRewards ? (
|
||||
<ListItemText secondary={t('Your compensations')}>
|
||||
<Grid container>
|
||||
<Grid item xs={9}>
|
||||
<Typography>{`${robot.earnedRewards} Sats`}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={3}>
|
||||
<Button
|
||||
disabled={robot.earnedRewards === 0}
|
||||
onClick={() => {
|
||||
setOpenClaimRewards(true);
|
||||
}}
|
||||
variant='contained'
|
||||
size='small'
|
||||
>
|
||||
{t('Claim')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItemText>
|
||||
) : (
|
||||
<form noValidate style={{ maxWidth: 270 }}>
|
||||
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<Grid item style={{ display: 'flex', maxWidth: 160 }}>
|
||||
<TextField
|
||||
error={Boolean(badInvoice)}
|
||||
helperText={badInvoice ?? ''}
|
||||
label={t('Invoice for {{amountSats}} Sats', {
|
||||
amountSats: robot.earnedRewards,
|
||||
})}
|
||||
size='small'
|
||||
value={rewardInvoice}
|
||||
onChange={(e) => {
|
||||
setRewardInvoice(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 80 }}>
|
||||
<Button
|
||||
sx={{ maxHeight: 38 }}
|
||||
onClick={(e) => {
|
||||
handleSubmitInvoiceClicked(e, rewardInvoice);
|
||||
}}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
type='submit'
|
||||
>
|
||||
{t('Submit')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{weblnEnabled ? (
|
||||
<Grid container style={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex', maxWidth: 240 }}>
|
||||
<Button
|
||||
sx={{ maxHeight: 38, minWidth: 230 }}
|
||||
onClick={(e) => {
|
||||
handleWeblnInvoiceClicked(e);
|
||||
}}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
size='small'
|
||||
type='submit'
|
||||
>
|
||||
{t('Generate with Webln')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{showRewardsSpinner && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{withdrawn && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Typography color='primary' variant='body2'>
|
||||
<b>{t('There it goes!')}</b>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default RobotInfo;
|
@ -1,10 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Select, MenuItem, useTheme, Grid, Typography } from '@mui/material';
|
||||
import type Language from '../../models/Language.model';
|
||||
import {
|
||||
Select,
|
||||
MenuItem,
|
||||
useTheme,
|
||||
Grid,
|
||||
Typography,
|
||||
type SelectChangeEvent,
|
||||
} from '@mui/material';
|
||||
|
||||
import Flags from 'country-flag-icons/react/3x2';
|
||||
import { CataloniaFlag, BasqueCountryFlag } from '../Icons';
|
||||
import type { Language } from '../../models';
|
||||
|
||||
const menuLanuguages = [
|
||||
{ name: 'English', i18nCode: 'en', flag: Flags.US },
|
||||
@ -31,18 +38,18 @@ interface SelectLanguageProps {
|
||||
setLanguage: (lang: Language) => void;
|
||||
}
|
||||
|
||||
const SelectLanguage = ({ language, setLanguage }: SelectLanguageProps): JSX.Element => {
|
||||
const SelectLanguage: React.FC<SelectLanguageProps> = ({ language, setLanguage }) => {
|
||||
const theme = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const flagProps = {
|
||||
width: 1.5 * theme.typography.fontSize,
|
||||
height: 1.5 * theme.typography.fontSize,
|
||||
};
|
||||
|
||||
const handleChangeLang = function (e: any) {
|
||||
const handleChangeLang = function (e: SelectChangeEvent): void {
|
||||
setLanguage(e.target.value);
|
||||
i18n.changeLanguage(e.target.value);
|
||||
void i18n.changeLanguage(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -32,10 +32,9 @@ import SwapCalls from '@mui/icons-material/SwapCalls';
|
||||
|
||||
interface SettingsFormProps {
|
||||
dense?: boolean;
|
||||
showNetwork?: boolean;
|
||||
}
|
||||
|
||||
const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps): JSX.Element => {
|
||||
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
||||
const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@ -176,8 +175,8 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
|
||||
</ListItemIcon>
|
||||
<Slider
|
||||
value={settings.fontSize}
|
||||
min={settings.frontend == 'basic' ? 12 : 10}
|
||||
max={settings.frontend == 'basic' ? 16 : 14}
|
||||
min={settings.frontend === 'basic' ? 12 : 10}
|
||||
max={settings.frontend === 'basic' ? 16 : 14}
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const fontSize = e.target.value;
|
||||
@ -215,7 +214,6 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
|
||||
</ToggleButtonGroup>
|
||||
</ListItem>
|
||||
|
||||
{showNetwork ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Link />
|
||||
@ -236,9 +234,6 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
97
frontend/src/components/TorConnection/index.tsx
Normal file
97
frontend/src/components/TorConnection/index.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, CircularProgress, Tooltip } from '@mui/material';
|
||||
import { TorIcon } from '../Icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AppContext, type AppContextProps } from '../contexts/AppContext';
|
||||
|
||||
interface TorIndicatorProps {
|
||||
color: 'inherit' | 'error' | 'warning' | 'success' | 'primary' | 'secondary' | 'info' | undefined;
|
||||
tooltipOpen?: boolean | undefined;
|
||||
title: string;
|
||||
progress: boolean;
|
||||
}
|
||||
|
||||
const TorIndicator = ({
|
||||
color,
|
||||
tooltipOpen = undefined,
|
||||
title,
|
||||
progress,
|
||||
}: TorIndicatorProps): JSX.Element => {
|
||||
return (
|
||||
<Tooltip
|
||||
open={tooltipOpen}
|
||||
enterTouchDelay={0}
|
||||
enterDelay={1000}
|
||||
placement='right'
|
||||
title={title}
|
||||
>
|
||||
<Box sx={{ display: 'inline-flex', position: 'fixed', left: '0.5em', top: '0.5em' }}>
|
||||
{progress ? (
|
||||
<>
|
||||
<CircularProgress color={color} thickness={6} size={22} />
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<TorIcon color={color} sx={{ width: 20, height: 20 }} />
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box>
|
||||
<TorIcon color={color} sx={{ width: 20, height: 20 }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TorConnectionBadge = (): JSX.Element => {
|
||||
const { torStatus } = useContext<AppContextProps>(AppContext);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (window?.NativeRobosats == null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (torStatus === 'NOTINIT') {
|
||||
return (
|
||||
<TorIndicator
|
||||
color='primary'
|
||||
progress={true}
|
||||
tooltipOpen={true}
|
||||
title={t('Initializing TOR daemon')}
|
||||
/>
|
||||
);
|
||||
} else if (torStatus === 'STARTING') {
|
||||
return (
|
||||
<TorIndicator
|
||||
color='warning'
|
||||
progress={true}
|
||||
tooltipOpen={true}
|
||||
title={t('Connecting to TOR network')}
|
||||
/>
|
||||
);
|
||||
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
|
||||
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
||||
} else {
|
||||
return (
|
||||
<TorIndicator
|
||||
color='error'
|
||||
progress={false}
|
||||
tooltipOpen={true}
|
||||
title={t('Connection error')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TorConnectionBadge;
|
@ -16,10 +16,10 @@ const BondStatus = ({ status, isMaker }: BondStatusProps): JSX.Element => {
|
||||
let color = 'primary';
|
||||
if (status === 'unlocked') {
|
||||
Icon = LockOpen;
|
||||
color = theme.palette.mode == 'dark' ? 'lightgreen' : 'green';
|
||||
color = theme.palette.mode === 'dark' ? 'lightgreen' : 'green';
|
||||
} else if (status === 'settled') {
|
||||
Icon = Balance;
|
||||
color = theme.palette.mode == 'dark' ? 'lightred' : 'red';
|
||||
color = theme.palette.mode === 'dark' ? 'lightred' : 'red';
|
||||
}
|
||||
|
||||
if (status === 'hide') {
|
||||
|
@ -5,7 +5,7 @@ import { type Order } from '../../models';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
interface CancelButtonProps {
|
||||
order: Order;
|
||||
order: Order | null;
|
||||
onClickCancel: () => void;
|
||||
openCancelDialog: () => void;
|
||||
openCollabCancelDialog: () => void;
|
||||
@ -22,10 +22,12 @@ const CancelButton = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showCancelButton =
|
||||
(order.is_maker && [0, 1, 2].includes(order.status)) || [3, 6, 7].includes(order.status);
|
||||
const showCollabCancelButton = [8, 9].includes(order.status) && !order.asked_for_cancel;
|
||||
(order?.is_maker && [0, 1, 2].includes(order?.status)) ||
|
||||
[3, 6, 7].includes(order?.status ?? -1);
|
||||
const showCollabCancelButton = [8, 9].includes(order?.status ?? -1) && !order?.asked_for_cancel;
|
||||
const noConfirmation =
|
||||
(order.is_maker && [0, 1, 2].includes(order.status)) || (order.is_taker && order.status === 3);
|
||||
(order?.is_maker && [0, 1, 2].includes(order?.status ?? -1)) ||
|
||||
(order?.is_taker && order?.status === 3);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
@ -4,21 +4,21 @@ import { Alert } from '@mui/material';
|
||||
import { type Order } from '../../models';
|
||||
|
||||
interface CollabCancelAlertProps {
|
||||
order: Order;
|
||||
order: Order | null;
|
||||
}
|
||||
|
||||
const CollabCancelAlert = ({ order }: CollabCancelAlertProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
let text = '';
|
||||
if (order.pending_cancel) {
|
||||
if (order?.pending_cancel) {
|
||||
text = t('{{nickname}} is asking for a collaborative cancel', {
|
||||
nickname: order.is_maker ? order.taker_nick : order.maker_nick,
|
||||
nickname: order?.is_maker ? order?.taker_nick : order?.maker_nick,
|
||||
});
|
||||
} else if (order.asked_for_cancel) {
|
||||
} else if (order?.asked_for_cancel) {
|
||||
text = t('You asked for a collaborative cancellation');
|
||||
}
|
||||
|
||||
return text != '' ? (
|
||||
return text !== '' ? (
|
||||
<Alert severity='warning' style={{ width: '100%' }}>
|
||||
{text}
|
||||
</Alert>
|
||||
|
@ -16,7 +16,7 @@ import { LoadingButton } from '@mui/lab';
|
||||
interface ConfirmFiatReceivedDialogProps {
|
||||
open: boolean;
|
||||
loadingButton: boolean;
|
||||
order: Order;
|
||||
order: Order | null;
|
||||
onClose: () => void;
|
||||
onConfirmClick: () => void;
|
||||
}
|
||||
@ -29,8 +29,10 @@ export const ConfirmFiatReceivedDialog = ({
|
||||
onConfirmClick,
|
||||
}: ConfirmFiatReceivedDialogProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const currencyCode = currencies[order.currency.toString()];
|
||||
const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4)));
|
||||
const currencyCode = currencies[order?.currency.toString()];
|
||||
const amount = pn(
|
||||
parseFloat(parseFloat(order?.amount).toFixed(order?.currency === 1000 ? 8 : 4)),
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
|
@ -16,7 +16,7 @@ import { LoadingButton } from '@mui/lab';
|
||||
interface ConfirmFiatSentDialogProps {
|
||||
open: boolean;
|
||||
loadingButton: boolean;
|
||||
order: Order;
|
||||
order: Order | null;
|
||||
onClose: () => void;
|
||||
onConfirmClick: () => void;
|
||||
}
|
||||
@ -29,8 +29,10 @@ export const ConfirmFiatSentDialog = ({
|
||||
onConfirmClick,
|
||||
}: ConfirmFiatSentDialogProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const currencyCode = currencies[order.currency.toString()];
|
||||
const amount = pn(parseFloat(parseFloat(order.amount).toFixed(order.currency == 1000 ? 8 : 4)));
|
||||
const currencyCode = currencies[order?.currency.toString()];
|
||||
const amount = pn(
|
||||
parseFloat(parseFloat(order?.amount).toFixed(order?.currency === 1000 ? 8 : 4)),
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
|
@ -14,7 +14,6 @@ import { LoadingButton } from '@mui/lab';
|
||||
interface ConfirmUndoFiatSentDialogProps {
|
||||
open: boolean;
|
||||
loadingButton: boolean;
|
||||
order: Order;
|
||||
onClose: () => void;
|
||||
onConfirmClick: () => void;
|
||||
}
|
||||
@ -36,7 +35,7 @@ export const ConfirmUndoFiatSentDialog = ({
|
||||
}
|
||||
}, [time, open]);
|
||||
|
||||
const onClick = () => {
|
||||
const onClick = (): void => {
|
||||
onConfirmClick();
|
||||
setTime(300);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Grid, useTheme, Tooltip, Button } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { Grid, Tooltip, Button } from '@mui/material';
|
||||
import { ExportIcon } from '../../../Icons';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -15,7 +15,6 @@ interface Props {
|
||||
|
||||
const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@ -62,7 +61,7 @@ const ChatBottom: React.FC<Props> = ({ orderId, setAudit, audit, createJsonFile
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
saveAsJson('complete_log_chat_' + orderId + '.json', createJsonFile());
|
||||
saveAsJson(`complete_log_chat_${orderId}.json`, createJsonFile());
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '1.4em', height: '1.4em' }}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Tooltip, TextField, Grid, Paper, Typography } from '@mui/material';
|
||||
import { Button, TextField, Grid, Paper, Typography } from '@mui/material';
|
||||
import { encryptMessage, decryptMessage } from '../../../../pgp';
|
||||
import { AuditPGPDialog } from '../../../Dialogs';
|
||||
import { websocketClient, type WebsocketConnection } from '../../../../services/Websocket';
|
||||
@ -48,6 +48,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
|
||||
const [connected, setConnected] = useState<boolean>(false);
|
||||
@ -64,7 +65,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected && robot.avatarLoaded) {
|
||||
if (!connected && garage.getRobot().avatarLoaded) {
|
||||
connectWebsocket();
|
||||
}
|
||||
}, [connected, robot]);
|
||||
@ -87,18 +88,18 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > messageCount) {
|
||||
audio.play();
|
||||
void audio.play();
|
||||
setMessageCount(messages.length);
|
||||
}
|
||||
}, [messages, messageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverMessages) {
|
||||
if (serverMessages.length > 0) {
|
||||
serverMessages.forEach(onMessage);
|
||||
}
|
||||
}, [serverMessages]);
|
||||
|
||||
const connectWebsocket = () => {
|
||||
const connectWebsocket = (): void => {
|
||||
websocketClient
|
||||
.open(
|
||||
`ws://${window.location.host}/ws/chat/${orderId}/?token_sha256_hex=${sha256(robot.token)}`,
|
||||
@ -121,6 +122,9 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
connection.onError(() => {
|
||||
setConnected(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setConnected(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -139,14 +143,14 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
const onMessage: (message: any) => void = (message) => {
|
||||
const dataFromServer = JSON.parse(message.data);
|
||||
|
||||
if (dataFromServer && !receivedIndexes.includes(dataFromServer.index)) {
|
||||
if (dataFromServer != null && !receivedIndexes.includes(dataFromServer.index)) {
|
||||
setReceivedIndexes((prev) => [...prev, dataFromServer.index]);
|
||||
setPeerConnected(dataFromServer.peer_connected);
|
||||
// If we receive a public key other than ours (our peer key!)
|
||||
if (
|
||||
connection != null &&
|
||||
dataFromServer.message.substring(0, 36) == `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message != robot.pubKey
|
||||
dataFromServer.message.substring(0, 36) === `-----BEGIN PGP PUBLIC KEY BLOCK-----` &&
|
||||
dataFromServer.message !== robot.pubKey
|
||||
) {
|
||||
setPeerPubKey(dataFromServer.message);
|
||||
connection.send({
|
||||
@ -155,10 +159,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
});
|
||||
}
|
||||
// If we receive an encrypted message
|
||||
else if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) {
|
||||
decryptMessage(
|
||||
else if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) {
|
||||
void decryptMessage(
|
||||
dataFromServer.message.split('\\').join('\n'),
|
||||
dataFromServer.user_nick == userNick ? robot.pubKey : peerPubKey,
|
||||
dataFromServer.user_nick === userNick ? robot.pubKey : peerPubKey,
|
||||
robot.encPrivKey,
|
||||
robot.token,
|
||||
).then((decryptedData) => {
|
||||
@ -166,27 +170,25 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
setLastSent(decryptedData.decryptedMessage === lastSent ? '----BLANK----' : lastSent);
|
||||
setMessages((prev) => {
|
||||
const existingMessage = prev.find((item) => item.index === dataFromServer.index);
|
||||
if (existingMessage) {
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
const x: EncryptedChatMessage = {
|
||||
index: dataFromServer.index,
|
||||
encryptedMessage: dataFromServer.message.split('\\').join('\n'),
|
||||
plainTextMessage: decryptedData.decryptedMessage,
|
||||
plainTextMessage: String(decryptedData.decryptedMessage),
|
||||
validSignature: decryptedData.validSignature,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: dataFromServer.time,
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
return [...prev, x].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// We allow plaintext communication. The user must write # to start
|
||||
// If we receive an plaintext message
|
||||
else if (dataFromServer.message.substring(0, 1) == '#') {
|
||||
else if (dataFromServer.message.substring(0, 1) === '#') {
|
||||
setMessages((prev: EncryptedChatMessage[]) => {
|
||||
const existingMessage = prev.find(
|
||||
(item) => item.plainTextMessage === dataFromServer.message,
|
||||
@ -194,32 +196,30 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
const x: EncryptedChatMessage = {
|
||||
index: prev.length + 0.001,
|
||||
encryptedMessage: dataFromServer.message,
|
||||
plainTextMessage: dataFromServer.message,
|
||||
validSignature: false,
|
||||
userNick: dataFromServer.user_nick,
|
||||
time: new Date().toString(),
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
return [...prev, x].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onButtonClicked = (e: any) => {
|
||||
if (robot.token && value.includes(robot.token)) {
|
||||
const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
|
||||
if (robot.token !== undefined && value.includes(robot.token)) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
setValue('');
|
||||
}
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (connection != null && value.substring(0, 1) == '#') {
|
||||
else if (connection != null && value.substring(0, 1) === '#') {
|
||||
connection.send({
|
||||
message: value,
|
||||
nick: userNick,
|
||||
@ -228,7 +228,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
// Else if message is not empty send message
|
||||
else if (value != '') {
|
||||
else if (value !== '') {
|
||||
setValue('');
|
||||
setWaitingEcho(true);
|
||||
setLastSent(value);
|
||||
@ -236,7 +236,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
.then((encryptedMessage) => {
|
||||
if (connection != null) {
|
||||
connection.send({
|
||||
message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
message: String(encryptedMessage).split('\n').join('\\'),
|
||||
nick: userNick,
|
||||
});
|
||||
}
|
||||
@ -263,17 +263,17 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={robot.pubKey || ''}
|
||||
own_enc_priv_key={robot.encPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={robot.token || ''}
|
||||
ownPubKey={robot.pubKey ?? ''}
|
||||
ownEncPrivKey={robot.encPrivKey ?? ''}
|
||||
peerPubKey={peerPubKey ?? 'Not received yet'}
|
||||
passphrase={robot.token ?? ''}
|
||||
onClickBack={() => {
|
||||
setAudit(false);
|
||||
}}
|
||||
/>
|
||||
<Grid item>
|
||||
<ChatHeader
|
||||
connected={connected && (peerPubKey ? true : false)}
|
||||
connected={connected && Boolean(peerPubKey)}
|
||||
peerConnected={peerConnected}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
@ -326,7 +326,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}}
|
||||
helperText={
|
||||
connected
|
||||
? peerPubKey
|
||||
? peerPubKey !== undefined
|
||||
? null
|
||||
: t('Waiting for peer public key...')
|
||||
: t('Connecting...')
|
||||
@ -341,7 +341,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
|
||||
<Button
|
||||
fullWidth={true}
|
||||
disabled={!connected || waitingEcho || !peerPubKey}
|
||||
disabled={!connected || waitingEcho || peerPubKey === undefined}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
|
@ -47,6 +47,8 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
}: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { origin, hostUrl, settings } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, focusedCoordinator } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
|
||||
const [peerConnected, setPeerConnected] = useState<boolean>(false);
|
||||
@ -62,13 +64,13 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > messageCount) {
|
||||
audio.play();
|
||||
void audio.play();
|
||||
setMessageCount(messages.length);
|
||||
}
|
||||
}, [messages, messageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverMessages.length > 0 && peerPubKey) {
|
||||
if (serverMessages.length > 0 && peerPubKey !== undefined) {
|
||||
serverMessages.forEach(onMessage);
|
||||
}
|
||||
}, [serverMessages, peerPubKey]);
|
||||
@ -80,20 +82,26 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
}, [chatOffset]);
|
||||
|
||||
const loadMessages: () => void = () => {
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, {
|
||||
.get(url, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, {
|
||||
tokenSHA256: robot.tokenSHA256,
|
||||
})
|
||||
.then((results: any) => {
|
||||
if (results) {
|
||||
if (results != null) {
|
||||
setPeerConnected(results.peer_connected);
|
||||
setPeerPubKey(results.peer_pubkey.split('\\').join('\n'));
|
||||
setServerMessages(results.messages);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const createJsonFile: () => object = () => {
|
||||
const createJsonFile = (): object => {
|
||||
return {
|
||||
credentials: {
|
||||
own_public_key: robot.pubKey,
|
||||
@ -105,13 +113,13 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
};
|
||||
};
|
||||
|
||||
const onMessage: (dataFromServer: ServerMessage) => void = (dataFromServer) => {
|
||||
if (dataFromServer) {
|
||||
const onMessage = (dataFromServer: ServerMessage): void => {
|
||||
if (dataFromServer != null) {
|
||||
// If we receive an encrypted message
|
||||
if (dataFromServer.message.substring(0, 27) == `-----BEGIN PGP MESSAGE-----`) {
|
||||
decryptMessage(
|
||||
if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) {
|
||||
void decryptMessage(
|
||||
dataFromServer.message.split('\\').join('\n'),
|
||||
dataFromServer.nick == userNick ? robot.pubKey : peerPubKey,
|
||||
dataFromServer.nick === userNick ? robot.pubKey : peerPubKey,
|
||||
robot.encPrivKey,
|
||||
robot.token,
|
||||
).then((decryptedData) => {
|
||||
@ -122,60 +130,59 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
const x: EncryptedChatMessage = {
|
||||
index: dataFromServer.index,
|
||||
encryptedMessage: dataFromServer.message.split('\\').join('\n'),
|
||||
plainTextMessage: decryptedData.decryptedMessage,
|
||||
validSignature: decryptedData.validSignature,
|
||||
userNick: dataFromServer.nick,
|
||||
time: dataFromServer.time,
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
return [...prev, x].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// We allow plaintext communication. The user must write # to start
|
||||
// If we receive an plaintext message
|
||||
else if (dataFromServer.message.substring(0, 1) == '#') {
|
||||
setMessages((prev) => {
|
||||
else if (dataFromServer.message.substring(0, 1) === '#') {
|
||||
setMessages((prev: EncryptedChatMessage[]) => {
|
||||
const existingMessage = prev.find(
|
||||
(item) => item.plainTextMessage === dataFromServer.message,
|
||||
);
|
||||
if (existingMessage) {
|
||||
if (existingMessage != null) {
|
||||
return prev;
|
||||
} else {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
const x: EncryptedChatMessage = {
|
||||
index: prev.length + 0.001,
|
||||
encryptedMessage: dataFromServer.message,
|
||||
plainTextMessage: dataFromServer.message,
|
||||
validSignature: false,
|
||||
userNick: dataFromServer.nick,
|
||||
time: new Date().toString(),
|
||||
} as EncryptedChatMessage,
|
||||
].sort((a, b) => a.index - b.index);
|
||||
};
|
||||
return [...prev, x].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onButtonClicked = (e: any) => {
|
||||
if (robot.token && value.includes(robot.token)) {
|
||||
const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
|
||||
if (robot.token !== undefined && value.includes(robot.token)) {
|
||||
alert(
|
||||
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
|
||||
);
|
||||
setValue('');
|
||||
}
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (value.substring(0, 1) == '#') {
|
||||
else if (value.substring(0, 1) === '#') {
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
url,
|
||||
`/api/chat/`,
|
||||
{
|
||||
PGP_message: value,
|
||||
@ -186,7 +193,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
)
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
if (response.messages) {
|
||||
if (response.messages != null) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
setServerMessages(response.messages);
|
||||
}
|
||||
@ -198,17 +205,20 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
});
|
||||
}
|
||||
// Else if message is not empty send message
|
||||
else if (value != '') {
|
||||
else if (value !== '') {
|
||||
setWaitingEcho(true);
|
||||
setLastSent(value);
|
||||
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token)
|
||||
.then((encryptedMessage) => {
|
||||
const { url } = federation
|
||||
.getCoordinator(focusedCoordinator)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
url,
|
||||
`/api/chat/`,
|
||||
{
|
||||
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
PGP_message: String(encryptedMessage).split('\n').join('\\'),
|
||||
order_id: orderId,
|
||||
offset: lastIndex,
|
||||
},
|
||||
@ -217,7 +227,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
if (response.messages) {
|
||||
if (response.messages != null) {
|
||||
setServerMessages(response.messages);
|
||||
}
|
||||
}
|
||||
@ -249,10 +259,10 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
}}
|
||||
orderId={Number(orderId)}
|
||||
messages={messages}
|
||||
own_pub_key={robot.pubKey || ''}
|
||||
own_enc_priv_key={robot.encPrivKey || ''}
|
||||
peer_pub_key={peerPubKey || 'Not received yet'}
|
||||
passphrase={robot.token || ''}
|
||||
ownPubKey={robot.pubKey ?? ''}
|
||||
ownEncPrivKey={robot.encPrivKey ?? ''}
|
||||
peerPubKey={peerPubKey ?? 'Not received yet'}
|
||||
passphrase={robot.token ?? ''}
|
||||
onClickBack={() => {
|
||||
setAudit(false);
|
||||
}}
|
||||
@ -260,7 +270,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
|
||||
<Grid item>
|
||||
<ChatHeader
|
||||
connected={peerPubKey ? true : false}
|
||||
connected={Boolean(peerPubKey)}
|
||||
peerConnected={peerConnected}
|
||||
turtleMode={turtleMode}
|
||||
setTurtleMode={setTurtleMode}
|
||||
@ -320,7 +330,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
</Grid>
|
||||
<Grid item alignItems='stretch' style={{ display: 'flex' }} xs={3}>
|
||||
<Button
|
||||
disabled={waitingEcho || !peerPubKey}
|
||||
disabled={waitingEcho || peerPubKey === undefined}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
|
@ -19,7 +19,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl }) => {
|
||||
const [showPGP, setShowPGP] = useState<boolean>();
|
||||
const [showPGP, setShowPGP] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
@ -122,7 +122,7 @@ const MessageCard: React.FC<Props> = ({ message, isTaker, userConnected, baseUrl
|
||||
showPGP ? (
|
||||
<a>
|
||||
{' '}
|
||||
{message.time} <br /> {'Valid signature: ' + message.validSignature} <br />{' '}
|
||||
{message.time} <br /> {`Valid signature: ${String(message.validSignature)}`} <br />{' '}
|
||||
{message.encryptedMessage}{' '}
|
||||
</a>
|
||||
) : (
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Grid, TextField, Checkbox, Tooltip, FormControlLabel } from '@mui/material';
|
||||
import { Order } from '../../../models';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { EncryptedChatMessage } from '../EncryptedChat';
|
||||
|
||||
export interface DisputeForm {
|
||||
statement: string;
|
||||
@ -43,7 +41,7 @@ export const DisputeStatementForm = ({
|
||||
>
|
||||
<Grid item>
|
||||
<TextField
|
||||
error={dispute.badStatement != ''}
|
||||
error={dispute.badStatement !== ''}
|
||||
helperText={dispute.badStatement}
|
||||
label={t('Submit dispute statement')}
|
||||
required
|
||||
|
@ -33,7 +33,7 @@ import { apiClient } from '../../../services/api';
|
||||
import { systemClient } from '../../../services/System';
|
||||
|
||||
import lnproxies from '../../../../static/lnproxies.json';
|
||||
let filteredProxies: { [key: string]: any }[] = [];
|
||||
let filteredProxies: Array<Record<string, any>> = [];
|
||||
export interface LightningForm {
|
||||
invoice: string;
|
||||
amount: number;
|
||||
@ -95,23 +95,23 @@ export const LightningPayoutForm = ({
|
||||
const [loadingLnproxy, setLoadingLnproxy] = useState<boolean>(false);
|
||||
const [noMatchingLnProxies, setNoMatchingLnProxies] = useState<string>('');
|
||||
|
||||
const computeInvoiceAmount = function () {
|
||||
const computeInvoiceAmount = (): number => {
|
||||
const tradeAmount = order.trade_satoshis;
|
||||
return Math.floor(tradeAmount - tradeAmount * (lightning.routingBudgetPPM / 1000000));
|
||||
};
|
||||
|
||||
const validateInvoice = function (invoice: string, targetAmount: number) {
|
||||
const validateInvoice = (invoice: string, targetAmount: number): string => {
|
||||
try {
|
||||
const decoded = decode(invoice);
|
||||
const invoiceAmount = Math.floor(decoded.sections[2].value / 1000);
|
||||
if (targetAmount != invoiceAmount) {
|
||||
if (targetAmount !== invoiceAmount) {
|
||||
return 'Invalid invoice amount';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err.toString();
|
||||
return `${error.substring(0, 100)}${error.length > 100 ? '...' : ''}`;
|
||||
return `${String(error).substring(0, 100)}${error.length > 100 ? '...' : ''}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -122,14 +122,14 @@ export const LightningPayoutForm = ({
|
||||
amount,
|
||||
lnproxyAmount: amount - lightning.lnproxyBudgetSats,
|
||||
routingBudgetSats:
|
||||
lightning.routingBudgetSats == undefined
|
||||
lightning.routingBudgetSats === undefined
|
||||
? Math.ceil((amount / 1000000) * lightning.routingBudgetPPM)
|
||||
: lightning.routingBudgetSats,
|
||||
});
|
||||
}, [lightning.routingBudgetPPM]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lightning.invoice != '') {
|
||||
if (lightning.invoice !== '') {
|
||||
setLightning({
|
||||
...lightning,
|
||||
badInvoice: validateInvoice(lightning.invoice, lightning.amount),
|
||||
@ -138,7 +138,7 @@ export const LightningPayoutForm = ({
|
||||
}, [lightning.invoice, lightning.amount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lightning.lnproxyInvoice != '') {
|
||||
if (lightning.lnproxyInvoice !== '') {
|
||||
setLightning({
|
||||
...lightning,
|
||||
badLnproxy: validateInvoice(lightning.lnproxyInvoice, lightning.lnproxyAmount),
|
||||
@ -151,15 +151,15 @@ export const LightningPayoutForm = ({
|
||||
let internetNetwork: 'Clearnet' | 'I2P' | 'TOR' = 'Clearnet';
|
||||
useEffect(() => {
|
||||
bitcoinNetwork = settings?.network ?? 'mainnet';
|
||||
if (settings.host?.includes('.i2p')) {
|
||||
if (settings.host?.includes('.i2p') === true) {
|
||||
internetNetwork = 'I2P';
|
||||
} else if (settings.host?.includes('.onion') || window.NativeRobosats != undefined) {
|
||||
} else if (settings.host?.includes('.onion') === true || window.NativeRobosats !== undefined) {
|
||||
internetNetwork = 'TOR';
|
||||
}
|
||||
|
||||
filteredProxies = lnproxies
|
||||
.filter((node) => node.relayType == internetNetwork)
|
||||
.filter((node) => node.network == bitcoinNetwork);
|
||||
.filter((node) => node.relayType === internetNetwork)
|
||||
.filter((node) => node.network === bitcoinNetwork);
|
||||
}, [settings]);
|
||||
|
||||
// if "use lnproxy" checkbox is enabled, but there are no matching proxies, enter error state
|
||||
@ -175,21 +175,21 @@ export const LightningPayoutForm = ({
|
||||
}
|
||||
}, [lightning.useLnproxy]);
|
||||
|
||||
const fetchLnproxy = function () {
|
||||
const fetchLnproxy = function (): void {
|
||||
setLoadingLnproxy(true);
|
||||
let body: { invoice: string; description: string; routing_msat?: string } = {
|
||||
const body: { invoice: string; description: string; routing_msat?: string } = {
|
||||
invoice: lightning.lnproxyInvoice,
|
||||
description: '',
|
||||
};
|
||||
if (lightning.lnproxyBudgetSats > 0) {
|
||||
body['routing_msat'] = String(lightning.lnproxyBudgetSats * 1000);
|
||||
body.routing_msat = String(lightning.lnproxyBudgetSats * 1000);
|
||||
}
|
||||
apiClient
|
||||
.post(filteredProxies[lightning.lnproxyServer]['url'], '', body)
|
||||
.post(filteredProxies[lightning.lnproxyServer].url, '', body)
|
||||
.then((data) => {
|
||||
if (data.reason) {
|
||||
if (data.reason !== undefined) {
|
||||
setLightning({ ...lightning, badLnproxy: data.reason });
|
||||
} else if (data.proxy_invoice) {
|
||||
} else if (data.proxy_invoice !== undefined) {
|
||||
setLightning({ ...lightning, invoice: data.proxy_invoice, badLnproxy: '' });
|
||||
} else {
|
||||
setLightning({ ...lightning, badLnproxy: 'Unknown lnproxy response' });
|
||||
@ -203,7 +203,7 @@ export const LightningPayoutForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdvancedOptions = function (checked: boolean) {
|
||||
const handleAdvancedOptions = function (checked: boolean): void {
|
||||
if (checked) {
|
||||
setLightning({
|
||||
...lightning,
|
||||
@ -218,7 +218,7 @@ export const LightningPayoutForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onProxyBudgetChange = function (e) {
|
||||
const onProxyBudgetChange = function (e: React.ChangeEventHandler<HTMLInputElement>): void {
|
||||
if (isFinite(e.target.value) && e.target.value >= 0) {
|
||||
let lnproxyBudgetSats;
|
||||
let lnproxyBudgetPPM;
|
||||
@ -238,7 +238,7 @@ export const LightningPayoutForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onRoutingBudgetChange = function (e) {
|
||||
const onRoutingBudgetChange = function (e: React.ChangeEventHandler<HTMLInputElement>): void {
|
||||
const tradeAmount = order.trade_satoshis;
|
||||
if (isFinite(e.target.value) && e.target.value >= 0) {
|
||||
let routingBudgetSats;
|
||||
@ -261,7 +261,7 @@ export const LightningPayoutForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const lnProxyBudgetHelper = function () {
|
||||
const lnProxyBudgetHelper = function (): string {
|
||||
let text = '';
|
||||
if (lightning.lnproxyBudgetSats < 0) {
|
||||
text = 'Must be positive';
|
||||
@ -271,7 +271,7 @@ export const LightningPayoutForm = ({
|
||||
return text;
|
||||
};
|
||||
|
||||
const routingBudgetHelper = function () {
|
||||
const routingBudgetHelper = function (): string {
|
||||
let text = '';
|
||||
if (lightning.routingBudgetSats < 0) {
|
||||
text = 'Must be positive';
|
||||
@ -334,12 +334,12 @@ export const LightningPayoutForm = ({
|
||||
<TextField
|
||||
sx={{ width: '14em' }}
|
||||
disabled={!lightning.advancedOptions}
|
||||
error={routingBudgetHelper() != ''}
|
||||
error={routingBudgetHelper() !== ''}
|
||||
helperText={routingBudgetHelper()}
|
||||
label={t('Routing Budget')}
|
||||
required={true}
|
||||
value={
|
||||
lightning.routingBudgetUnit == 'PPM'
|
||||
lightning.routingBudgetUnit === 'PPM'
|
||||
? lightning.routingBudgetPPM
|
||||
: lightning.routingBudgetSats
|
||||
}
|
||||
@ -353,7 +353,7 @@ export const LightningPayoutForm = ({
|
||||
setLightning({
|
||||
...lightning,
|
||||
routingBudgetUnit:
|
||||
lightning.routingBudgetUnit == 'PPM' ? 'Sats' : 'PPM',
|
||||
lightning.routingBudgetUnit === 'PPM' ? 'Sats' : 'PPM',
|
||||
});
|
||||
}}
|
||||
>
|
||||
@ -382,11 +382,11 @@ export const LightningPayoutForm = ({
|
||||
>
|
||||
<div>
|
||||
<FormControlLabel
|
||||
onChange={(e) => {
|
||||
onChange={(e, checked) => {
|
||||
setLightning({
|
||||
...lightning,
|
||||
useLnproxy: e.target.checked,
|
||||
invoice: e.target.checked ? '' : lightning.invoice,
|
||||
useLnproxy: checked,
|
||||
invoice: checked ? '' : lightning.invoice,
|
||||
});
|
||||
}}
|
||||
checked={lightning.useLnproxy}
|
||||
@ -419,7 +419,7 @@ export const LightningPayoutForm = ({
|
||||
spacing={1}
|
||||
>
|
||||
<Grid item>
|
||||
<FormControl error={noMatchingLnProxies != ''}>
|
||||
<FormControl error={noMatchingLnProxies !== ''}>
|
||||
<InputLabel id='select-label'>{t('Server')}</InputLabel>
|
||||
<Select
|
||||
sx={{ width: '14em' }}
|
||||
@ -436,7 +436,7 @@ export const LightningPayoutForm = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{noMatchingLnProxies != '' ? (
|
||||
{noMatchingLnProxies !== '' ? (
|
||||
<FormHelperText>{t(noMatchingLnProxies)}</FormHelperText>
|
||||
) : (
|
||||
<></>
|
||||
@ -448,11 +448,11 @@ export const LightningPayoutForm = ({
|
||||
<TextField
|
||||
sx={{ width: '14em' }}
|
||||
disabled={!lightning.useLnproxy}
|
||||
error={lnProxyBudgetHelper() != ''}
|
||||
error={lnProxyBudgetHelper() !== ''}
|
||||
helperText={lnProxyBudgetHelper()}
|
||||
label={t('Proxy Budget')}
|
||||
value={
|
||||
lightning.lnproxyBudgetUnit == 'PPM'
|
||||
lightning.lnproxyBudgetUnit === 'PPM'
|
||||
? lightning.lnproxyBudgetPPM
|
||||
: lightning.lnproxyBudgetSats
|
||||
}
|
||||
@ -466,7 +466,7 @@ export const LightningPayoutForm = ({
|
||||
setLightning({
|
||||
...lightning,
|
||||
lnproxyBudgetUnit:
|
||||
lightning.lnproxyBudgetUnit == 'PPM' ? 'Sats' : 'PPM',
|
||||
lightning.lnproxyBudgetUnit === 'PPM' ? 'Sats' : 'PPM',
|
||||
});
|
||||
}}
|
||||
>
|
||||
@ -520,9 +520,9 @@ export const LightningPayoutForm = ({
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
disabled={!lightning.useLnproxy}
|
||||
error={lightning.badLnproxy != ''}
|
||||
error={lightning.badLnproxy !== ''}
|
||||
FormHelperTextProps={{ style: { wordBreak: 'break-all' } }}
|
||||
helperText={lightning.badLnproxy ? t(lightning.badLnproxy) : ''}
|
||||
helperText={lightning.badLnproxy !== '' ? t(lightning.badLnproxy) : ''}
|
||||
label={t('Invoice to wrap')}
|
||||
required
|
||||
value={lightning.lnproxyInvoice}
|
||||
@ -541,8 +541,8 @@ export const LightningPayoutForm = ({
|
||||
fullWidth={true}
|
||||
sx={lightning.useLnproxy ? { borderRadius: 0 } : {}}
|
||||
disabled={lightning.useLnproxy}
|
||||
error={lightning.badInvoice != ''}
|
||||
helperText={lightning.badInvoice ? t(lightning.badInvoice) : ''}
|
||||
error={lightning.badInvoice !== ''}
|
||||
helperText={lightning.badInvoice !== '' ? t(lightning.badInvoice) : ''}
|
||||
FormHelperTextProps={{ style: { wordBreak: 'break-all' } }}
|
||||
label={lightning.useLnproxy ? t('Wrapped invoice') : t('Payout Lightning Invoice')}
|
||||
required
|
||||
@ -566,8 +566,8 @@ export const LightningPayoutForm = ({
|
||||
loading={loadingLnproxy}
|
||||
disabled={
|
||||
lightning.lnproxyInvoice.length < 20 ||
|
||||
noMatchingLnProxies != '' ||
|
||||
lightning.badLnproxy != ''
|
||||
noMatchingLnProxies !== '' ||
|
||||
lightning.badLnproxy !== ''
|
||||
}
|
||||
onClick={fetchLnproxy}
|
||||
variant='outlined'
|
||||
@ -580,7 +580,7 @@ export const LightningPayoutForm = ({
|
||||
)}
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
disabled={lightning.invoice.length < 20 || lightning.badInvoice != ''}
|
||||
disabled={lightning.invoice.length < 20 || lightning.badInvoice !== ''}
|
||||
onClick={() => {
|
||||
onClickSubmit(lightning.invoice);
|
||||
}}
|
||||
|
@ -39,7 +39,7 @@ export const OnchainPayoutForm = ({
|
||||
const invalidFee = onchain.miningFee < minMiningFee || onchain.miningFee > maxMiningFee;
|
||||
const costPerVByte = 280;
|
||||
|
||||
const handleMiningFeeChange = (e) => {
|
||||
const handleMiningFeeChange = (e: React.ChangeEventHandler<HTMLInputElement>): void => {
|
||||
const miningFee = Number(e.target.value);
|
||||
setOnchain({ ...onchain, miningFee });
|
||||
};
|
||||
@ -67,12 +67,9 @@ export const OnchainPayoutForm = ({
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
pn(Math.floor((order.invoice_amount * order.swap_fee_rate) / 100)) +
|
||||
' Sats (' +
|
||||
order.swap_fee_rate +
|
||||
'%)'
|
||||
}
|
||||
primary={`${pn(Math.floor((order.invoice_amount * order.swap_fee_rate) / 100))} Sats (${
|
||||
order.swap_fee_rate
|
||||
}%)`}
|
||||
secondary={t('Swap fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
@ -81,12 +78,9 @@ export const OnchainPayoutForm = ({
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
pn(Math.floor(Math.max(minMiningFee, onchain.miningFee) * costPerVByte)) +
|
||||
' Sats (' +
|
||||
Math.max(minMiningFee, onchain.miningFee) +
|
||||
' Sats/vByte)'
|
||||
}
|
||||
primary={`${pn(
|
||||
Math.floor(Math.max(minMiningFee, onchain.miningFee) * costPerVByte),
|
||||
)} Sats (${Math.max(minMiningFee, onchain.miningFee)} Sats/vByte)`}
|
||||
secondary={t('Mining fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
@ -115,8 +109,8 @@ export const OnchainPayoutForm = ({
|
||||
<Grid container direction='row' justifyContent='center' alignItems='flex-start' spacing={0}>
|
||||
<Grid item xs={7}>
|
||||
<TextField
|
||||
error={onchain.badAddress != ''}
|
||||
helperText={onchain.badAddress ? t(onchain.badAddress) : ''}
|
||||
error={onchain.badAddress !== ''}
|
||||
helperText={onchain.badAddress !== '' ? t(onchain.badAddress) : ''}
|
||||
label={t('Bitcoin Address')}
|
||||
required
|
||||
value={onchain.address}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user