mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
Merge pull request #922 from RoboSats/the-federation-layer-v0.6.0
The federation layer v0.6.0
This commit is contained in:
commit
d30ea462db
@ -1,7 +1,7 @@
|
||||
exclude: '(api|chat|control)/migrations/.*'
|
||||
repos:
|
||||
- repo: 'https://github.com/pre-commit/pre-commit-hooks'
|
||||
rev: v2.3.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
@ -104,7 +104,7 @@ def send_devfund_donation(order_id, proceeds, reason):
|
||||
|
||||
order = Order.objects.get(id=order_id)
|
||||
coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="NoAlias")
|
||||
donation_fraction = max(0.05, config("DEVFUND", cast=float, default=0.2))
|
||||
donation_fraction = min(1.0, max(0.00, config("DEVFUND", cast=float, default=0.2)))
|
||||
message = f"Devfund donation; {coordinator_alias}; {order}; {donation_fraction}; {reason};"
|
||||
num_satoshis = int(proceeds * donation_fraction)
|
||||
routing_budget_sats = int(max(5, num_satoshis * 0.000_1))
|
||||
|
@ -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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
_RoboSats Federation Basis v0.6.0~2_
|
||||
_RoboSats Federation Basis v0.6.0~3_
|
||||
|
||||
## Introduction
|
||||
RoboSats is a Free and Open Source project, anyone can spin up a new RoboSats backend instance. This is in fact ideal, given that the more backend instances there are, the more decentralized RoboSats becomes and the harder it is to stop. However, this decentralization creates some challenges:
|
||||
@ -96,6 +96,16 @@ Some of these badges can be objectively measured and awarded. Other badges rely
|
||||
|
||||
We also envision more badges in the future, for example milestones by number of trades coordinated (200, 1K, 5K, 25K, 100K, etc).
|
||||
|
||||
## New coordinator limits.
|
||||
The RoboSats client app limits the size of order tha robots can place on newly joined coordinators. This way new coordinators can show their worth with smaller orders before handling all order sizes. The client allows orders of up to 250K Sats for completely new coordinators. The client will increase the size limite by 30% every 2016 blocks (that is, 2 weeks, same as the difficulty adjustment). After 6 months, a coordinator will be considered "mature" and able to host any order size. Coordinator that gained the "Founder" badge by joining before the v0.6.0 release are considered "mature".
|
||||
|
||||
## New Coordinator Order Size Limits
|
||||
The RoboSats client application imposes limits on the order size that robots can place on newly established coordinators. This mechanism allows new coordinators to demonstrate their capabilities with progressively larger orders.
|
||||
|
||||
New coordinators are initially restricted to hosting orders of up to 250,000 Sats. Over time, the order size limit increases. Specifically, the limit grows by 30% every 2016 blocks (2 weeks), the same cadence of the Bitcoin mining difficulty adjustment.
|
||||
|
||||
After six months, or approximately 12,288 blocks, a coordinator reaches maturity and the app grants it the ability to handle orders of any size. Notably, coordinators that earned the "Founder" badge upon joining prior to the v0.6.0 release are considered mature and can process any order size immediately.
|
||||
|
||||
## Timeline
|
||||
|
||||
In a sense the RoboSats federation is already online. New coordinators can gradually join. Eventually, the RoboSats "Experimental" coordinator that is run by the development team will be phased out. The RoboSats Federated client app can be used already in `robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion`. You can also run a pre-release of the v0.6.0 selfhosted client.
|
||||
|
@ -19,12 +19,22 @@
|
||||
"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/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "variableLike",
|
||||
"format": ["camelCase", "snake_case", "PascalCase", "UPPER_CASE"],
|
||||
"leadingUnderscore": "allow"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
|
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.6.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.6.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
@ -47,6 +47,7 @@
|
||||
"react-smooth-image": "^1.1.0",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"robo-identities-wasm": "^0.1.0",
|
||||
"simple-plist": "^1.3.1",
|
||||
"webln": "^0.3.2",
|
||||
"websocket": "^1.0.34"
|
||||
@ -66,14 +67,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.35.1",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"copy-webpack-plugin": "^12.0.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^36.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
@ -2235,9 +2236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
|
||||
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.10",
|
||||
@ -3367,9 +3368,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
|
||||
"integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
|
||||
"version": "7.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.12.tgz",
|
||||
"integrity": "sha512-3kaHiNm9khCAo0pVe0RenketDSFoZGAlVZ4zDjB/QNZV0XiCj+sh1zkX0VVhQPgYJDlBEzAag+MHJ1tU3vf0Zw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
@ -3445,8 +3446,8 @@
|
||||
"integrity": "sha512-/bdWZabexuz+1rKG15XryxiMGb5D0XVx65NU7CZYKm/1+HuUzc0FX9smKEa/YVZnLSNsAp6SULIyPZtAKE+3AA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@mui/base": "^5.0.0-beta.22",
|
||||
"@mui/utils": "^5.14.16",
|
||||
"@mui/base": "^5.0.0-beta.20",
|
||||
"@mui/utils": "^5.14.14",
|
||||
"@types/react-transition-group": "^4.4.8",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
@ -3464,7 +3465,7 @@
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@mui/system": "^5.8.0",
|
||||
"date-fns": "^2.25.0 || ^3.2.0",
|
||||
"date-fns": "^2.25.0",
|
||||
"date-fns-jalali": "^2.13.0-0",
|
||||
"dayjs": "^1.10.7",
|
||||
"luxon": "^3.0.2",
|
||||
@ -3505,16 +3506,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.32",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.32.tgz",
|
||||
"integrity": "sha512-4VptvYeLUYMJhZapWBkD50GmKfOc0XT381KJcTK3ncZYIl8MdBhpR6l8jOyeP5cixUPBJhstjrnmQEAHjCLriw==",
|
||||
"version": "5.0.0-beta.21",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.21.tgz",
|
||||
"integrity": "sha512-eTKWx3WV/nwmRUK4z4K1MzlMyWCsi3WJ3RtV4DiXZeRh4qd4JCyp1Zzzi8Wv9xM4dEBmqQntFoei716PzwmFfA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.8",
|
||||
"@floating-ui/react-dom": "^2.0.5",
|
||||
"@mui/types": "^7.2.13",
|
||||
"@mui/utils": "^5.15.5",
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@floating-ui/react-dom": "^2.0.2",
|
||||
"@mui/types": "^7.2.7",
|
||||
"@mui/utils": "^5.14.15",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.1.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -3522,7 +3523,7 @@
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
"url": "https://opencollective.com/mui"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
@ -3536,9 +3537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers/node_modules/clsx": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
|
||||
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -5743,9 +5744,9 @@
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||
},
|
||||
"node_modules/copy-webpack-plugin": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
|
||||
"integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.1.tgz",
|
||||
"integrity": "sha512-dhMfjJMYKDmmbG6Yn2pRSs1g8FgeQRtbE/JM6VAM9Xouk3KO1UVrwlLHLXxaI5F+o9WgnRfhFZzY9eV34O2gZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
@ -6889,9 +6890,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
|
||||
"integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz",
|
||||
"integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
@ -14216,6 +14217,26 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/robo-identities-wasm": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/robo-identities-wasm/-/robo-identities-wasm-0.1.0.tgz",
|
||||
"integrity": "sha512-q6+1Vgq+8d2F5k8Nqm39qwQJYe9uTC7TlR3NbBQ6k2ImBNccdAEoZgb0ikKjN59cK4MvqejlgBV1ybaLXoHbhA=="
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
|
||||
"integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"execa": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.5.4",
|
||||
"version": "0.6.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "node --max-old-space-size=4096 ./node_modules/.bin/webpack --watch --progress --mode development",
|
||||
"test": "jest",
|
||||
"build": "webpack --mode production",
|
||||
"lint": "eslint src/**/*.{js,ts,tsx}",
|
||||
"lint:fix": "eslint --fix 'src/**/*.{js,ts,tsx}'",
|
||||
"lint": "eslint src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint --fix 'src/**/*.{ts,tsx}'",
|
||||
"format": "prettier --write '**/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc"
|
||||
},
|
||||
"keywords": [],
|
||||
@ -29,14 +29,14 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.35.1",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"copy-webpack-plugin": "^12.0.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^36.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
@ -86,6 +86,7 @@
|
||||
"react-smooth-image": "^1.1.0",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"robo-identities-wasm": "^0.1.0",
|
||||
"simple-plist": "^1.3.1",
|
||||
"webln": "^0.3.2",
|
||||
"websocket": "^1.0.34"
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { StrictMode, Suspense } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Main from './basic/Main';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { AppContext, useAppStore } from './contexts/AppContext';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import HostAlert from './components/HostAlert';
|
||||
import TorConnectionBadge from './components/TorConnection';
|
||||
|
||||
@ -11,21 +10,25 @@ import i18n from './i18n/Web';
|
||||
|
||||
import { systemClient } from './services/System';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import { AppContextProvider } from './contexts/AppContext';
|
||||
import { GarageContextProvider } from './contexts/GarageContext';
|
||||
import { FederationContextProvider } from './contexts/FederationContext';
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
const store = useAppStore();
|
||||
return (
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback='loading'>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppContext.Provider value={store}>
|
||||
<ThemeProvider theme={store.theme}>
|
||||
<CssBaseline />
|
||||
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
</AppContext.Provider>
|
||||
<AppContextProvider>
|
||||
<GarageContextProvider>
|
||||
<FederationContextProvider>
|
||||
<CssBaseline />
|
||||
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
|
||||
<Main />
|
||||
</FederationContextProvider>
|
||||
</GarageContextProvider>
|
||||
</AppContextProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
@ -33,7 +36,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);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, ButtonGroup, Dialog, Box } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -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, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const BookPage = (): JSX.Element => {
|
||||
const { robot, fetchBook, windowSize, setDelay, setOrder } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { windowSize } = useContext<UseAppStoreType>(AppContext);
|
||||
const { setDelay, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage } = 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);
|
||||
setDelay(10000);
|
||||
};
|
||||
|
||||
const onOrderClicked = function (id: number) {
|
||||
if (robot.avatarLoaded) {
|
||||
navigate('/order/' + id);
|
||||
onViewOrder();
|
||||
const onOrderClicked = function (id: number, shortAlias: string): void {
|
||||
if (garage.getSlot()?.hashId) {
|
||||
setDelay(10000);
|
||||
setCurrentOrderId({ id, shortAlias });
|
||||
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,14 @@
|
||||
import React, { useContext } 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 { GarageContext, type UseGarageStoreType } from '../contexts/GarageContext';
|
||||
|
||||
const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter;
|
||||
|
||||
@ -35,39 +29,20 @@ 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 } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<RobotAvatar
|
||||
style={{ display: 'none' }}
|
||||
nickname={robot.nickname}
|
||||
baseUrl={baseUrl}
|
||||
onLoad={() => {
|
||||
setRobot((robot) => {
|
||||
return { ...robot, avatarLoaded: true };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<RobotAvatar style={{ display: 'none' }} hashId={garage.getSlot()?.hashId} />
|
||||
<Notifications
|
||||
order={order}
|
||||
page={page}
|
||||
openProfile={() => {
|
||||
setOpen({ ...closeAll, profile: true });
|
||||
}}
|
||||
rewards={robot.earnedRewards}
|
||||
windowWidth={windowSize.width}
|
||||
rewards={garage.getSlot()?.getRobot()?.earnedRewards}
|
||||
windowWidth={windowSize?.width}
|
||||
/>
|
||||
{settings.network === 'testnet' ? (
|
||||
<TestnetTypography color='secondary' align='center'>
|
||||
@ -87,7 +62,7 @@ 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 />
|
||||
@ -105,7 +80,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 +95,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 +105,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 +125,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 +135,7 @@ const Main: React.FC = () => {
|
||||
/>
|
||||
</Routes>
|
||||
</MainBox>
|
||||
<NavBar width={windowSize.width} height={navbarHeight} />
|
||||
<NavBar />
|
||||
<MainDialogs />
|
||||
</Router>
|
||||
);
|
||||
|
@ -1,117 +1,98 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import React, { useContext } 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, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
export interface OpenDialogs {
|
||||
more: boolean;
|
||||
learn: boolean;
|
||||
community: boolean;
|
||||
info: boolean;
|
||||
coordinator: boolean;
|
||||
stats: boolean;
|
||||
coordinator: string;
|
||||
warning: boolean;
|
||||
exchange: boolean;
|
||||
client: boolean;
|
||||
update: boolean;
|
||||
profile: boolean;
|
||||
notice: boolean;
|
||||
}
|
||||
|
||||
const MainDialogs = (): JSX.Element => {
|
||||
const { open, setOpen, info, limits, robot, setRobot, setCurrentOrder, baseUrl } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
const [maxAmount, setMaxAmount] = useState<string>('...loading...');
|
||||
|
||||
useEffect(() => {
|
||||
if (limits.list[1000]) {
|
||||
setMaxAmount(pn(limits.list[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]);
|
||||
const { open, setOpen, settings, clientVersion } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
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}
|
||||
onClose={() => {
|
||||
setOpen({ ...open, profile: false });
|
||||
}}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
/>
|
||||
<CoordinatorDialog
|
||||
open={Boolean(open.coordinator)}
|
||||
network={settings.network}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
shortAlias={open.coordinator}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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,14 @@ import BookTable from '../../components/BookTable';
|
||||
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { NoRobotDialog } from '../../components/Dialogs';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type 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 { federation, setDelay, setCurrentOrderId } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -23,11 +26,12 @@ 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,
|
||||
mode: fav.mode,
|
||||
coordinator: 'any',
|
||||
},
|
||||
premium: Number(maker.premium) ?? null,
|
||||
paymentMethods: maker.paymentMethods,
|
||||
@ -39,7 +43,7 @@ const MakerPage = (): JSX.Element => {
|
||||
},
|
||||
});
|
||||
}, [
|
||||
book.orders,
|
||||
federation.book,
|
||||
fav,
|
||||
maker.premium,
|
||||
maker.amount,
|
||||
@ -48,15 +52,11 @@ const MakerPage = (): JSX.Element => {
|
||||
maker.paymentMethods,
|
||||
]);
|
||||
|
||||
const onViewOrder = function () {
|
||||
setOrder(undefined);
|
||||
setDelay(10000);
|
||||
};
|
||||
|
||||
const onOrderClicked = function (id: number) {
|
||||
if (robot.avatarLoaded) {
|
||||
navigate('/order/' + id);
|
||||
onViewOrder();
|
||||
const onOrderClicked = function (id: number, shortAlias: string): void {
|
||||
if (garage.getSlot()?.hashId) {
|
||||
setDelay(10000);
|
||||
setCurrentOrderId({ id, shortAlias });
|
||||
navigate(`/order/${shortAlias}/${id}`);
|
||||
} else {
|
||||
setOpenNoRobot(true);
|
||||
}
|
||||
@ -69,7 +69,9 @@ const MakerPage = (): JSX.Element => {
|
||||
onClose={() => {
|
||||
setOpenNoRobot(false);
|
||||
}}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
<Grid item>
|
||||
<Collapse in={matches.length > 0 && showMatches}>
|
||||
@ -103,8 +105,9 @@ const MakerPage = (): JSX.Element => {
|
||||
}}
|
||||
>
|
||||
<MakerForm
|
||||
onOrderCreated={(id) => {
|
||||
navigate('/order/' + id);
|
||||
onOrderCreated={(shortAlias, id) => {
|
||||
setCurrentOrderId({ id, shortAlias });
|
||||
navigate(`/order/${shortAlias}/${id}`);
|
||||
}}
|
||||
disableRequest={matches.length > 0 && !showMatches}
|
||||
collapseAll={showMatches}
|
||||
@ -115,7 +118,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,28 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface NavBarProps {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
const {
|
||||
robot,
|
||||
page,
|
||||
settings,
|
||||
setPage,
|
||||
setSlideDirection,
|
||||
open,
|
||||
setOpen,
|
||||
currentOrder,
|
||||
baseUrl,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
const NavBar = (): JSX.Element => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { page, setPage, settings, setSlideDirection, open, setOpen, windowSize, navbarHeight } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
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.getSlot()?.hashId ? '0.9em' : '0.13em',
|
||||
minWidth: '1em',
|
||||
}
|
||||
: { position: 'relative', bottom: '1em', minWidth: '2em' };
|
||||
|
||||
const pagesPosition = {
|
||||
@ -53,19 +48,23 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
settings: 5,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// re-render on orde rand robot updated at for latest orderId in tab
|
||||
}, [robotUpdatedAt]);
|
||||
|
||||
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]);
|
||||
|
||||
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 +72,19 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const changePage = function (mouseEvent: any, newPage: Page) {
|
||||
if (newPage === 'none') {
|
||||
return null;
|
||||
} else {
|
||||
const changePage = function (mouseEvent: any, newPage: Page): void {
|
||||
if (newPage !== 'none') {
|
||||
const slot = garage.getSlot();
|
||||
handleSlideDirection(page, newPage);
|
||||
setPage(newPage);
|
||||
const param = newPage === 'order' ? currentOrder ?? '' : '';
|
||||
const shortAlias = String(slot?.activeShortAlias);
|
||||
const activeOrderId = slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId;
|
||||
const lastOrderId = slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId;
|
||||
const param =
|
||||
newPage === 'order' ? `${shortAlias}/${String(activeOrderId ?? lastOrderId)}` : '';
|
||||
if (newPage === 'order') {
|
||||
setCurrentOrderId({ id: activeOrderId ?? lastOrderId, shortAlias });
|
||||
}
|
||||
setTimeout(() => {
|
||||
navigate(`/${newPage}/${param}`);
|
||||
}, theme.transitions.duration.leavingScreen * 3);
|
||||
@ -88,35 +93,42 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(closeAll);
|
||||
}, [page]);
|
||||
}, [page, setOpen]);
|
||||
|
||||
const slot = garage.getSlot();
|
||||
|
||||
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={slot?.nickname === null}
|
||||
onClick={() => {
|
||||
setOpen({ ...closeAll, profile: !open.profile });
|
||||
}}
|
||||
icon={
|
||||
robot.nickname && robot.avatarLoaded ? (
|
||||
slot?.hashId ? (
|
||||
<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}
|
||||
hashId={slot?.hashId}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@ -150,7 +162,7 @@ const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
|
||||
sx={tabSx}
|
||||
label={smallBar ? undefined : t('Order')}
|
||||
value='order'
|
||||
disabled={!robot.avatarLoaded || currentOrder == undefined}
|
||||
disabled={!slot?.getRobot()?.activeOrderId}
|
||||
icon={<Assignment />}
|
||||
iconPosition='start'
|
||||
/>
|
||||
@ -166,11 +178,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);
|
||||
}
|
||||
|
@ -6,86 +6,108 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import TradeBox from '../../components/TradeBox';
|
||||
import OrderDetails from '../../components/OrderDetails';
|
||||
|
||||
import { apiClient } from '../../services/api';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { AppContext, closeAll, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { WarningDialog } from '../../components/Dialogs';
|
||||
|
||||
const OrderPage = (): JSX.Element => {
|
||||
const {
|
||||
windowSize,
|
||||
info,
|
||||
order,
|
||||
robot,
|
||||
open,
|
||||
setOpen,
|
||||
acknowledgedWarning,
|
||||
setAcknowledgedWarning,
|
||||
settings,
|
||||
setOrder,
|
||||
clearOrder,
|
||||
currentOrder,
|
||||
setCurrentOrder,
|
||||
badOrder,
|
||||
setBadOrder,
|
||||
baseUrl,
|
||||
navbarHeight,
|
||||
hostUrl,
|
||||
origin,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, currentOrder, currentOrderId, setCurrentOrderId } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { badOrder } = 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 shortAlias = params.shortAlias;
|
||||
const coordinator = federation.getCoordinator(shortAlias ?? '');
|
||||
const { url, basePath } = coordinator.getEndpoint(
|
||||
settings.network,
|
||||
origin,
|
||||
settings.selfhostedClient,
|
||||
hostUrl,
|
||||
);
|
||||
|
||||
const renewOrder = function () {
|
||||
if (order != undefined) {
|
||||
const body = {
|
||||
type: order.type,
|
||||
currency: order.currency,
|
||||
amount: order.has_range ? null : order.amount,
|
||||
has_range: order.has_range,
|
||||
min_amount: order.min_amount,
|
||||
max_amount: order.max_amount,
|
||||
payment_method: order.payment_method,
|
||||
is_explicit: order.is_explicit,
|
||||
premium: order.is_explicit ? null : order.premium,
|
||||
satoshis: order.is_explicit ? order.satoshis : null,
|
||||
public_duration: order.public_duration,
|
||||
escrow_duration: order.escrow_duration,
|
||||
bond_size: order.bond_size,
|
||||
latitude: order.latitude,
|
||||
longitude: order.longitude,
|
||||
};
|
||||
apiClient
|
||||
.post(baseUrl, '/api/make/', body, { tokenSHA256: robot.tokenSHA256 })
|
||||
.then((data: any) => {
|
||||
if (data.bad_request) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.id) {
|
||||
navigate('/order/' + data.id);
|
||||
}
|
||||
});
|
||||
setBaseUrl(`${url}${basePath}`);
|
||||
|
||||
const orderId = Number(params.orderId);
|
||||
if (
|
||||
orderId &&
|
||||
currentOrderId.id !== orderId &&
|
||||
currentOrderId.shortAlias !== shortAlias &&
|
||||
shortAlias
|
||||
)
|
||||
setCurrentOrderId({ id: orderId, shortAlias });
|
||||
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
|
||||
}, [params, currentOrderId]);
|
||||
|
||||
const onClickCoordinator = function (): void {
|
||||
if (currentOrder?.shortAlias != null) {
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: currentOrder?.shortAlias };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startAgain = () => {
|
||||
const startAgain = (): void => {
|
||||
navigate('/robot');
|
||||
};
|
||||
|
||||
const orderDetailsSpace = currentOrder ? (
|
||||
<OrderDetails
|
||||
shortAlias={String(currentOrder.shortAlias)}
|
||||
currentOrder={currentOrder}
|
||||
onClickCoordinator={onClickCoordinator}
|
||||
onClickGenerateRobot={() => {
|
||||
navigate('/robot');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
const tradeBoxSpace = currentOrder ? (
|
||||
<TradeBox baseUrl={baseUrl} onStartAgain={startAgain} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{order == undefined && badOrder == undefined ? <CircularProgress /> : null}
|
||||
{badOrder != undefined ? (
|
||||
<WarningDialog
|
||||
open={open.warning}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
setAcknowledgedWarning(true);
|
||||
}}
|
||||
longAlias={federation.getCoordinator(params.shortAlias ?? '').longAlias}
|
||||
/>
|
||||
{currentOrder === 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 !== null && badOrder === undefined ? (
|
||||
currentOrder.is_participant ? (
|
||||
windowSize.width > doublePageWidth ? (
|
||||
// DOUBLE PAPER VIEW
|
||||
<Grid
|
||||
@ -105,14 +127,7 @@ const OrderPage = (): JSX.Element => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
/>
|
||||
{orderDetailsSpace}
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{ width: '21em' }}>
|
||||
@ -124,16 +139,7 @@ const OrderPage = (): JSX.Element => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<TradeBox
|
||||
order={order}
|
||||
robot={robot}
|
||||
settings={settings}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
onStartAgain={startAgain}
|
||||
/>
|
||||
{tradeBoxSpace}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -143,7 +149,7 @@ const OrderPage = (): JSX.Element => {
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', width: '21em' }}>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(mouseEvent, value) => {
|
||||
onChange={(_mouseEvent, value) => {
|
||||
setTab(value);
|
||||
}}
|
||||
variant='fullWidth'
|
||||
@ -160,28 +166,8 @@ const OrderPage = (): JSX.Element => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: tab == 'order' ? '' : 'none' }}>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
|
||||
<TradeBox
|
||||
order={order}
|
||||
robot={robot}
|
||||
settings={settings}
|
||||
setOrder={setOrder}
|
||||
setBadOrder={setBadOrder}
|
||||
baseUrl={baseUrl}
|
||||
onRenewOrder={renewOrder}
|
||||
onStartAgain={startAgain}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: tab === 'order' ? '' : 'none' }}>{orderDetailsSpace}</div>
|
||||
<div style={{ display: tab === 'contract' ? '' : 'none' }}>{tradeBoxSpace}</div>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
@ -194,14 +180,7 @@ const OrderPage = (): JSX.Element => {
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<OrderDetails
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
info={info}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onClickGenerateRobot={() => navigate('/robot')}
|
||||
/>
|
||||
{orderDetailsSpace}
|
||||
</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, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { GarageContext, type 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 { setPage } = 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);
|
||||
@ -59,11 +61,13 @@ const Onboarding = ({
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const slot = garage.getSlot();
|
||||
|
||||
return (
|
||||
<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 +105,7 @@ const Onboarding = ({
|
||||
autoFocusTarget='copyButton'
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
</Grid>
|
||||
@ -122,7 +124,6 @@ const Onboarding = ({
|
||||
onClick={() => {
|
||||
setStep('2');
|
||||
getGenerateRobot(inputToken);
|
||||
setRobot({ ...robot, nickname: undefined });
|
||||
}}
|
||||
variant='contained'
|
||||
size='large'
|
||||
@ -141,7 +142,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 +150,7 @@ const Onboarding = ({
|
||||
<Grid container direction='column' alignItems='center' spacing={1}>
|
||||
<Grid item>
|
||||
<Typography>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{slot?.hashId ? (
|
||||
t('This is your trading avatar')
|
||||
) : (
|
||||
<>
|
||||
@ -162,7 +163,7 @@ const Onboarding = ({
|
||||
|
||||
<Grid item sx={{ width: '13.5em' }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
hashId={slot?.hashId ?? ''}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
@ -174,11 +175,10 @@ const Onboarding = ({
|
||||
width: '12.4em',
|
||||
}}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{slot?.hashId ? (
|
||||
<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>{slot?.nickname}</b>
|
||||
<Bolt
|
||||
sx={{
|
||||
color: '#fcba03',
|
||||
@ -210,7 +210,7 @@ const Onboarding = ({
|
||||
</Grid>
|
||||
) : null}
|
||||
<Grid item>
|
||||
<Collapse in={!!(robot.avatarLoaded && robot.nickname)}>
|
||||
<Collapse in={!!slot?.hashId}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStep('3');
|
||||
@ -229,7 +229,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>
|
||||
@ -249,6 +249,7 @@ const Onboarding = ({
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
navigate('/offers');
|
||||
setPage('offers');
|
||||
}}
|
||||
>
|
||||
<Storefront /> <div style={{ width: '0.5em' }} />
|
||||
@ -258,6 +259,7 @@ const Onboarding = ({
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
navigate('/create');
|
||||
setPage('create');
|
||||
}}
|
||||
>
|
||||
<AddBox /> <div style={{ width: '0.5em' }} />
|
||||
|
@ -1,33 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, Typography } from '@mui/material';
|
||||
import { type Robot } from '../../models';
|
||||
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;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
getRecoverRobot: (token: string) => void;
|
||||
}
|
||||
|
||||
const Recovery = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
badToken,
|
||||
setView,
|
||||
setInputToken,
|
||||
getGenerateRobot,
|
||||
getRecoverRobot,
|
||||
}: RecoveryProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickRecover = () => {
|
||||
getGenerateRobot(inputToken);
|
||||
const onClickRecover = (): void => {
|
||||
getRecoverRobot(inputToken);
|
||||
setView('profile');
|
||||
};
|
||||
|
||||
@ -48,15 +43,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, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { FederationContext, type 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 } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage, robotUpdatedAt, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
@ -53,22 +55,30 @@ const RobotProfile = ({
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.nickname && robot.avatarLoaded) {
|
||||
const slot = garage.getSlot();
|
||||
if (slot?.hashId) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [robot]);
|
||||
}, [orderUpdatedAt, robotUpdatedAt, loading]);
|
||||
|
||||
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 = e.target.value;
|
||||
setInputToken(garage.getSlot()?.token ?? '');
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const slot = garage.getSlot();
|
||||
const robot = slot?.getRobot();
|
||||
|
||||
const loadingCoordinators = Object.values(slot?.robots ?? {}).filter(
|
||||
(robot) => robot.loading,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}>
|
||||
<Grid
|
||||
@ -80,7 +90,7 @@ const RobotProfile = ({
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
|
||||
{robot.avatarLoaded && robot.nickname ? (
|
||||
{slot?.hashId ? (
|
||||
<Typography align='center' component='h5' variant='h5'>
|
||||
<div
|
||||
style={{
|
||||
@ -99,7 +109,7 @@ const RobotProfile = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<b>{robot.nickname}</b>
|
||||
<b>{slot?.nickname}</b>
|
||||
{width < 19 ? null : (
|
||||
<Bolt
|
||||
sx={{
|
||||
@ -121,7 +131,7 @@ const RobotProfile = ({
|
||||
|
||||
<Grid item sx={{ width: `13.5em` }}>
|
||||
<RobotAvatar
|
||||
nickname={robot.nickname}
|
||||
hashId={slot?.hashId}
|
||||
smooth={true}
|
||||
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
|
||||
placeholderType='generating'
|
||||
@ -134,9 +144,8 @@ const RobotProfile = ({
|
||||
}}
|
||||
tooltip={t('This is your trading avatar')}
|
||||
tooltipPosition='top'
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{robot.found && !robot.lastOrderId ? (
|
||||
{robot?.found && Boolean(slot?.lastShortAlias) ? (
|
||||
<Typography align='center' variant='h6'>
|
||||
{t('Welcome back!')}
|
||||
</Typography>
|
||||
@ -145,27 +154,38 @@ const RobotProfile = ({
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{robot.activeOrderId && robot.avatarLoaded && robot.nickname ? (
|
||||
{loadingCoordinators > 0 && !Boolean(robot?.activeOrderId) ? (
|
||||
<Grid>
|
||||
<b>{t('Looking for orders!')}</b>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{Boolean(robot?.activeOrderId) && Boolean(slot?.hashId) ? (
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.activeOrderId}`);
|
||||
setCurrentOrderId({ id: robot?.activeOrderId, shortAlias: slot?.activeShortAlias });
|
||||
navigate(
|
||||
`/order/${String(slot?.activeShortAlias)}/${String(robot?.activeOrderId)}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })}
|
||||
{t('Active order #{{orderID}}', { orderID: robot?.activeOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{robot.lastOrderId && robot.avatarLoaded && robot.nickname ? (
|
||||
{Boolean(robot?.lastOrderId) && Boolean(slot?.hashId) ? (
|
||||
<Grid item container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate(`/order/${robot.lastOrderId}`);
|
||||
setCurrentOrderId({ id: robot?.lastOrderId, shortAlias: slot?.activeShortAlias });
|
||||
navigate(`/order/${String(slot?.lastShortAlias)}/${String(robot?.lastOrderId)}`);
|
||||
}}
|
||||
>
|
||||
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })}
|
||||
{t('Last order #{{orderID}}', { orderID: robot?.lastOrderId })}
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
@ -188,6 +208,13 @@ const RobotProfile = ({
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{!Boolean(robot?.activeOrderId) &&
|
||||
slot?.hashId &&
|
||||
!Boolean(robot?.lastOrderId) &&
|
||||
loadingCoordinators === 0 ? (
|
||||
<Grid item>{t('No existing orders found')}</Grid>
|
||||
) : null}
|
||||
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
@ -221,8 +248,6 @@ const RobotProfile = ({
|
||||
editable={false}
|
||||
label={t('Store your token safely')}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
</Grid>
|
||||
@ -246,7 +271,7 @@ const RobotProfile = ({
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={loading ? 'loading' : currentSlot}
|
||||
value={loading ? 'loading' : garage.currentSlot}
|
||||
onChange={handleChangeSlot}
|
||||
>
|
||||
{loading ? (
|
||||
@ -254,9 +279,9 @@ const RobotProfile = ({
|
||||
<Typography>{t('Building...')}</Typography>
|
||||
</MenuItem>
|
||||
) : (
|
||||
garage.slots.map((slot: Slot, index: number) => {
|
||||
Object.values(garage.slots).map((slot: Slot, index: number) => {
|
||||
return (
|
||||
<MenuItem key={index} value={index}>
|
||||
<MenuItem key={index} value={slot.token}>
|
||||
<Grid
|
||||
container
|
||||
direction='row'
|
||||
@ -267,17 +292,16 @@ const RobotProfile = ({
|
||||
>
|
||||
<Grid item>
|
||||
<RobotAvatar
|
||||
nickname={slot.robot.nickname}
|
||||
hashId={slot?.hashId}
|
||||
smooth={true}
|
||||
style={{ width: '2.6em', height: '2.6em' }}
|
||||
placeholderType='loading'
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant={windowSize.width < 26 ? 'caption' : undefined}>
|
||||
{slot.robot.nickname}
|
||||
{slot?.nickname}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -314,7 +338,6 @@ const RobotProfile = ({
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
garage.delete();
|
||||
setCurrentSlot(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 { type 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,23 +31,28 @@ const TokenInput = ({
|
||||
setInputToken,
|
||||
}: TokenInputProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const [showCopied, setShowCopied] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCopied(false);
|
||||
}, [inputToken]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCopied(false);
|
||||
}, [showCopied]);
|
||||
|
||||
if (loading) {
|
||||
return <LinearProgress sx={{ height: '0.8em' }} />;
|
||||
} 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 +70,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.getSlot()?.copiedToken ? 'inherit' : 'primary'}
|
||||
onClick={() => {
|
||||
systemClient.copyToClipboard(inputToken);
|
||||
setShowCopied(true);
|
||||
setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, 1000);
|
||||
setRobot((robot) => {
|
||||
return { ...robot, copiedToken: true };
|
||||
});
|
||||
garage.updateSlot({ copiedToken: true }, inputToken);
|
||||
}}
|
||||
>
|
||||
<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';
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
} from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Robot } from '../../models';
|
||||
import Onboarding from './Onboarding';
|
||||
import Welcome from './Welcome';
|
||||
import RobotProfile from './RobotProfile';
|
||||
@ -21,13 +20,16 @@ import { TorIcon } from '../../components/Icons';
|
||||
import { genKey } from '../../pgp';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { validateTokenEntropy } from '../../utils';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
const RobotPage = (): JSX.Element => {
|
||||
const { robot, setRobot, fetchRobot, torStatus, windowSize, baseUrl, settings } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { federation, sortedCoordinators } = 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 +37,19 @@ 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.currentSlot !== null ? 'profile' : 'welcome',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (robot.token) {
|
||||
setInputToken(robot.token);
|
||||
}
|
||||
const token = url_token ?? robot.token;
|
||||
if (robot.nickname == null && token) {
|
||||
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
|
||||
const token = urlToken ?? garage.currentSlot;
|
||||
if (token !== undefined && token !== null && page === 'robot') {
|
||||
setInputToken(token);
|
||||
if (window.NativeRobosats === undefined || torStatus === '"Done"') {
|
||||
getGenerateRobot(token);
|
||||
setView('profile');
|
||||
}
|
||||
}
|
||||
}, [torStatus]);
|
||||
}, [torStatus, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputToken.length < 20) {
|
||||
@ -61,26 +61,29 @@ 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) => {
|
||||
garage.createRobot(token, sortedCoordinators, {
|
||||
token,
|
||||
pubKey: key.publicKeyArmored,
|
||||
encPrivKey: key.encryptedPrivateKeyArmored,
|
||||
},
|
||||
newToken: token,
|
||||
slot,
|
||||
});
|
||||
void federation.fetchRobot(garage, token);
|
||||
garage.currentSlot = token;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logoutRobot = () => {
|
||||
const logoutRobot = (): void => {
|
||||
setInputToken('');
|
||||
setRobot(new Robot());
|
||||
garage.deleteSlot();
|
||||
};
|
||||
|
||||
if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) {
|
||||
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
|
||||
return (
|
||||
<Paper
|
||||
elevation={12}
|
||||
@ -146,39 +149,31 @@ const RobotPage = (): JSX.Element => {
|
||||
{view === 'onboarding' ? (
|
||||
<Onboarding
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'profile' ? (
|
||||
<RobotProfile
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
logoutRobot={logoutRobot}
|
||||
width={width}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{view === 'recovery' ? (
|
||||
<Recovery
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
getRecoverRobot={getGenerateRobot}
|
||||
/>
|
||||
) : null}
|
||||
</Paper>
|
||||
|
@ -1,7 +1,8 @@
|
||||
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';
|
||||
|
||||
const SettingsPage = (): JSX.Element => {
|
||||
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
|
||||
@ -12,7 +13,7 @@ const SettingsPage = (): JSX.Element => {
|
||||
elevation={12}
|
||||
sx={{
|
||||
padding: '0.6em',
|
||||
width: '21em',
|
||||
width: '20.5em',
|
||||
maxHeight: `${maxHeight}em`,
|
||||
overflow: 'auto',
|
||||
overflowX: 'clip',
|
||||
@ -20,7 +21,10 @@ const SettingsPage = (): JSX.Element => {
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<SettingsForm showNetwork={!(window.NativeRobosats === undefined)} />
|
||||
<SettingsForm />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FederationTable maxHeight={18} />
|
||||
</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';
|
@ -20,11 +20,13 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import SwapCalls from '@mui/icons-material/SwapCalls';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
|
||||
interface BookControlProps {
|
||||
width: number;
|
||||
paymentMethod: string[];
|
||||
setPaymentMethods: () => void;
|
||||
setPaymentMethods: (state: string[]) => void;
|
||||
}
|
||||
|
||||
const BookControl = ({
|
||||
@ -33,6 +35,7 @@ const BookControl = ({
|
||||
setPaymentMethods,
|
||||
}: BookControlProps): JSX.Element => {
|
||||
const { fav, setFav } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -41,22 +44,27 @@ 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 handleHostChange = function (e: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const coordinator = String(e.target.value);
|
||||
setFav({ ...fav, coordinator });
|
||||
};
|
||||
|
||||
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 });
|
||||
@ -195,7 +203,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}
|
||||
@ -216,14 +224,15 @@ 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=''
|
||||
isFilter={true}
|
||||
multiple={true}
|
||||
optionsDisplayLimit={1}
|
||||
/>
|
||||
</Grid>
|
||||
) : null}
|
||||
@ -245,7 +254,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' }}>
|
||||
@ -256,9 +265,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'}>
|
||||
@ -306,6 +315,65 @@ const BookControl = ({
|
||||
</Select>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
||||
{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('hosted by')}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Select
|
||||
autoWidth
|
||||
sx={{
|
||||
height: '2.3em',
|
||||
border: '0.5px solid',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '4px',
|
||||
borderColor: 'text.disabled',
|
||||
'&:hover': {
|
||||
borderColor: 'text.primary',
|
||||
},
|
||||
}}
|
||||
size='small'
|
||||
label={t('Select Host')}
|
||||
required={true}
|
||||
value={fav.coordinator}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={handleHostChange}
|
||||
>
|
||||
<MenuItem value='any'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<FlagWithProps code='ANY' />
|
||||
</div>
|
||||
</MenuItem>
|
||||
{Object.values(federation.coordinators).map((coordinator) =>
|
||||
coordinator.enabled ? (
|
||||
<MenuItem
|
||||
key={coordinator.shortAlias}
|
||||
value={coordinator.shortAlias}
|
||||
color='text.secondary'
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<RobotAvatar
|
||||
shortAlias={coordinator.shortAlias}
|
||||
style={{ width: '1.55em', height: '1.55em' }}
|
||||
smooth={true}
|
||||
small={true}
|
||||
/>
|
||||
</div>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
</Grid>
|
||||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Divider />
|
||||
</Box>
|
||||
|
@ -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, type 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,14 @@ const BookTable = ({
|
||||
showNoResults = true,
|
||||
onOrderClicked = () => null,
|
||||
}: BookTableProps): JSX.Element => {
|
||||
const { book, fetchBook, fav, baseUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { fav, setOpen } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const orders = orderList ?? book.orders;
|
||||
const orders = orderList ?? federation.book;
|
||||
|
||||
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
||||
pageSize: 0,
|
||||
page: 0,
|
||||
@ -133,10 +139,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 {
|
||||
@ -197,17 +203,21 @@ const BookTable = ({
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
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}
|
||||
hashId={params.row.maker_hash_id}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
orderType={params.row.type}
|
||||
statusColor={statusBadgeColor(params.row.maker_status)}
|
||||
tooltip={t(params.row.maker_status)}
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
@ -225,59 +235,114 @@ const BookTable = ({
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ position: 'relative', left: '-1.64em' }}>
|
||||
<ListItemButton style={{ cursor: 'pointer' }}>
|
||||
<RobotAvatar
|
||||
nickname={params.row.maker_nick}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
orderType={params.row.type}
|
||||
statusColor={statusBadgeColor(params.row.maker_status)}
|
||||
tooltip={t(params.row.maker_status)}
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<div
|
||||
style={{ position: 'relative', left: '-0.34em', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<RobotAvatar
|
||||
hashId={params.row.maker_hash_id}
|
||||
smooth={true}
|
||||
flipHorizontally={true}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
orderType={params.row.type}
|
||||
statusColor={statusBadgeColor(params.row.maker_status)}
|
||||
tooltip={t(params.row.maker_status)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const typeObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'type',
|
||||
headerName: t('Is'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) =>
|
||||
params.row.type
|
||||
? t(fav.mode === 'fiat' ? 'Seller' : 'Swapping Out')
|
||||
: t(fav.mode === 'fiat' ? 'Buyer' : 'Swapping In'),
|
||||
};
|
||||
}, [fav.mode]);
|
||||
const onClickCoordinator = function (shortAlias: string): void {
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: shortAlias };
|
||||
});
|
||||
};
|
||||
|
||||
const amountObj = useCallback((width: number) => {
|
||||
const coordinatorObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'amount',
|
||||
headerName: t('Amount'),
|
||||
type: 'number',
|
||||
field: 'coordinatorShortAlias',
|
||||
headerName: t('Host'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const amount = fav.mode === 'swap' ? params.row.amount * 100 : params.row.amount;
|
||||
const minAmount =
|
||||
fav.mode === 'swap' ? params.row.min_amount * 100 : params.row.min_amount;
|
||||
const maxAmount =
|
||||
fav.mode === 'swap' ? params.row.max_amount * 100 : params.row.max_amount;
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
{amountToString(amount, params.row.has_range, minAmount, maxAmount) +
|
||||
(fav.mode === 'swap' ? 'M Sats' : '')}
|
||||
</div>
|
||||
<ListItemButton
|
||||
style={{ cursor: 'pointer', position: 'relative', left: '-1.54em' }}
|
||||
onClick={() => {
|
||||
onClickCoordinator(params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
shortAlias={params.row.coordinatorShortAlias}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
smooth={true}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItemButton>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [fav.mode]);
|
||||
}, []);
|
||||
|
||||
const typeObj = useCallback(
|
||||
(width: number) => {
|
||||
return {
|
||||
field: 'type',
|
||||
headerName: t('Is'),
|
||||
width: width * fontSize,
|
||||
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')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[fav.mode],
|
||||
);
|
||||
|
||||
const amountObj = useCallback(
|
||||
(width: number) => {
|
||||
return {
|
||||
field: 'amount',
|
||||
headerName: t('Amount'),
|
||||
type: 'number',
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
const amount = fav.mode === 'swap' ? params.row.amount * 100 : params.row.amount;
|
||||
const minAmount =
|
||||
fav.mode === 'swap' ? params.row.min_amount * 100 : params.row.min_amount;
|
||||
const maxAmount =
|
||||
fav.mode === 'swap' ? params.row.max_amount * 100 : params.row.max_amount;
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{amountToString(amount, params.row.has_range, minAmount, maxAmount) +
|
||||
(fav.mode === 'swap' ? 'M Sats' : '')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[fav.mode],
|
||||
);
|
||||
|
||||
const currencyObj = useCallback((width: number) => {
|
||||
return {
|
||||
@ -285,7 +350,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 +359,9 @@ const BookTable = ({
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
{currencyCode}
|
||||
<div style={{ width: '0.3em' }} />
|
||||
@ -304,25 +372,33 @@ const BookTable = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const paymentObj = useCallback((width: number) => {
|
||||
return {
|
||||
field: 'payment_method',
|
||||
headerName: fav.mode === 'fiat' ? t('Payment Method') : t('Destination'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
size={1.7 * fontSize}
|
||||
text={params.row.payment_method}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}, [fav.mode]);
|
||||
const paymentObj = useCallback(
|
||||
(width: number) => {
|
||||
return {
|
||||
field: 'payment_method',
|
||||
headerName: fav.mode === 'fiat' ? t('Payment Method') : t('Destination'),
|
||||
width: width * fontSize,
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
size={1.7 * fontSize}
|
||||
text={params.row.payment_method}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[fav.mode],
|
||||
);
|
||||
|
||||
const paymentSmallObj = useCallback((width: number) => {
|
||||
return {
|
||||
@ -337,6 +413,9 @@ const BookTable = ({
|
||||
left: '-4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<PaymentStringAsIcons
|
||||
othersText={t('Others')}
|
||||
@ -356,9 +435,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 +463,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 +475,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 +488,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' }}>
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOrderClicked(params.row.id, params.row.coordinatorShortAlias);
|
||||
}}
|
||||
>
|
||||
<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 +517,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 +544,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 +587,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 +609,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 +631,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 +647,7 @@ const BookTable = ({
|
||||
return {
|
||||
amount: {
|
||||
priority: 1,
|
||||
order: 4,
|
||||
order: 5,
|
||||
normal: {
|
||||
width: fav.mode === 'swap' ? 9.5 : 6.5,
|
||||
object: amountObj,
|
||||
@ -532,7 +655,7 @@ const BookTable = ({
|
||||
},
|
||||
currency: {
|
||||
priority: 2,
|
||||
order: 5,
|
||||
order: 6,
|
||||
normal: {
|
||||
width: fav.mode === 'swap' ? 0 : 5.9,
|
||||
object: currencyObj,
|
||||
@ -540,7 +663,7 @@ const BookTable = ({
|
||||
},
|
||||
premium: {
|
||||
priority: 3,
|
||||
order: 11,
|
||||
order: 12,
|
||||
normal: {
|
||||
width: 6,
|
||||
object: premiumObj,
|
||||
@ -548,7 +671,7 @@ const BookTable = ({
|
||||
},
|
||||
payment_method: {
|
||||
priority: 4,
|
||||
order: 6,
|
||||
order: 7,
|
||||
normal: {
|
||||
width: 12.85,
|
||||
object: paymentObj,
|
||||
@ -570,9 +693,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 +711,7 @@ const BookTable = ({
|
||||
},
|
||||
expires_at: {
|
||||
priority: 7,
|
||||
order: 7,
|
||||
order: 8,
|
||||
normal: {
|
||||
width: 5,
|
||||
object: expiryObj,
|
||||
@ -588,7 +719,7 @@ const BookTable = ({
|
||||
},
|
||||
escrow_duration: {
|
||||
priority: 8,
|
||||
order: 8,
|
||||
order: 9,
|
||||
normal: {
|
||||
width: 4.8,
|
||||
object: timerObj,
|
||||
@ -596,7 +727,7 @@ const BookTable = ({
|
||||
},
|
||||
satoshis_now: {
|
||||
priority: 9,
|
||||
order: 9,
|
||||
order: 10,
|
||||
normal: {
|
||||
width: 6,
|
||||
object: satoshisObj,
|
||||
@ -612,7 +743,7 @@ const BookTable = ({
|
||||
},
|
||||
bond_size: {
|
||||
priority: 11,
|
||||
order: 10,
|
||||
order: 11,
|
||||
normal: {
|
||||
width: 4.2,
|
||||
object: bondObj,
|
||||
@ -620,7 +751,7 @@ const BookTable = ({
|
||||
},
|
||||
id: {
|
||||
priority: 12,
|
||||
order: 12,
|
||||
order: 13,
|
||||
normal: {
|
||||
width: 4.8,
|
||||
object: idObj,
|
||||
@ -629,7 +760,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 +775,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 +795,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>
|
||||
@ -688,7 +824,7 @@ const BookTable = ({
|
||||
<Grid item xs={6}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
fetchBook();
|
||||
void federation.updateBook();
|
||||
}}
|
||||
>
|
||||
<Refresh />
|
||||
@ -712,7 +848,7 @@ const BookTable = ({
|
||||
Toolbar?: (props: any) => JSX.Element;
|
||||
}
|
||||
|
||||
const NoResultsOverlay = function () {
|
||||
const NoResultsOverlay = function (): JSX.Element {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
@ -723,14 +859,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 +922,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) => {
|
||||
@ -800,15 +937,20 @@ const BookTable = ({
|
||||
paymentMethod: paymentMethods,
|
||||
setPaymentMethods,
|
||||
},
|
||||
loadingOverlay: {
|
||||
variant: 'determinate',
|
||||
value:
|
||||
((federation.exchange.enabledCoordinators -
|
||||
federation.exchange.loadingCoordinators) /
|
||||
federation.exchange.enabledCoordinators) *
|
||||
100,
|
||||
},
|
||||
}}
|
||||
paginationModel={paginationModel}
|
||||
pageSizeOptions={width < 22 ? [] : [0, defaultPageSize, defaultPageSize * 2, 50, 100]}
|
||||
onPaginationModelChange={(newPaginationModel) => {
|
||||
setPaginationModel(newPaginationModel);
|
||||
}}
|
||||
onRowClick={(params: any) => {
|
||||
onOrderClicked(params.row.id);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
@ -821,7 +963,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}
|
||||
@ -841,9 +983,6 @@ const BookTable = ({
|
||||
onPaginationModelChange={(newPaginationModel) => {
|
||||
setPaginationModel(newPaginationModel);
|
||||
}}
|
||||
onRowClick={(params: any) => {
|
||||
onOrderClicked(params.row.id);
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</Dialog>
|
||||
|
@ -20,19 +20,23 @@ import {
|
||||
} from '@mui/material';
|
||||
import { AddCircleOutline, RemoveCircleOutline } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { type PublicOrder, type Order } from '../../../models';
|
||||
import type PublicOrder 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,
|
||||
type 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 +46,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 +67,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
|
||||
order.base_amount =
|
||||
(order.price * limits.list[currencyCode].price) / limits.list[order.currency].price;
|
||||
if (order.coordinatorShortAlias != null) {
|
||||
const limits = federation.getCoordinator(order.coordinatorShortAlias).limits;
|
||||
const price = limits[currencyCode] ? limits[currencyCode].price : 0;
|
||||
order.base_amount = (order.price * price) / price;
|
||||
}
|
||||
return order;
|
||||
});
|
||||
setEnrichedOrders(enriched);
|
||||
}
|
||||
}, [limits.list, book.orders, currencyCode]);
|
||||
}, [coordinatorUpdatedAt, currencyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enrichedOrders.length > 0) {
|
||||
@ -82,10 +91,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 +102,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 +145,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
};
|
||||
|
||||
const generateSerie = (orders: PublicOrder[]): Datum[] => {
|
||||
if (center == undefined) {
|
||||
if (center === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -144,7 +153,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 +178,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 +206,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 +234,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 +248,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,8 +308,8 @@ const DepthChart: React.FC<DepthChartProps> = ({
|
||||
<Grid item>
|
||||
<Box justifyContent='center'>
|
||||
{xType === 'base_amount'
|
||||
? `${center} ${currencyDict[currencyCode]}`
|
||||
: `${center}%`}
|
||||
? `${center} ${String(currencyDict[currencyCode])}`
|
||||
: `${String(center.toPrecision(3))}%`}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
|
@ -12,9 +12,12 @@ import {
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import Map from '../../Map';
|
||||
import { AppContext, UseAppStoreType } from '../../../contexts/AppContext';
|
||||
import { PhotoSizeSelectActual } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FederationContext,
|
||||
type UseFederationStoreType,
|
||||
} from '../../../contexts/FederationContext';
|
||||
|
||||
interface MapChartProps {
|
||||
maxWidth: number;
|
||||
@ -32,7 +35,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 +84,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 +130,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,27 @@ 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,
|
||||
type 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 != null && baseUrl !== '' ? (
|
||||
<Paper elevation={12} style={{ padding: 10, width: 250 }}>
|
||||
<Grid container justifyContent='space-between'>
|
||||
<Grid item xs={3}>
|
||||
@ -28,6 +38,7 @@ const OrderTooltip: React.FC<OrderTooltipProps> = ({ order }) => {
|
||||
tooltip={t(order.maker_status)}
|
||||
baseUrl={baseUrl}
|
||||
small={true}
|
||||
hashId={order.maker_hash_id}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -11,18 +11,16 @@ import {
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from '@mui/material';
|
||||
import { pn } from '../../utils';
|
||||
|
||||
// Icons
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
interface Props {
|
||||
maxAmount: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
const AboutDialog = ({ open, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -64,7 +62,7 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'At no point, AnonymousAlice01 and BafflingBob02 have to entrust the bitcoin funds to each other. In case they have a conflict, RoboSats staff will help resolving the dispute.',
|
||||
'At no point, AnonymousAlice01 and BafflingBob02 have to entrust the bitcoin funds to each other. In case they have a conflict, the RoboSats coordinator will help resolving the dispute.',
|
||||
)}
|
||||
{t('You can find a step-by-step description of the trade pipeline in ')}
|
||||
<Link target='_blank' href='https://learn.robosats.com/docs/trade-pipeline/'>
|
||||
@ -80,6 +78,22 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion disableGutters={true}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{t('What is a coordinator?')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography component='div' variant='body2'>
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
'RoboSats is a decentralized exchange with multiple, fully redundant, trade coordinators. The coordinator provides the infrastructure for your trade: mantains the intermediary lightning node, does book keeping, and relays your encrypted chat messages. The coordinator is also the judge in case your order enters a dispute. The coordinator is a trusted role, make sure you trust your coordinator by exploring its profile, webpage, social media and the comments from other users online.',
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion disableGutters={true}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{t('What payment methods are accepted?')}</Typography>
|
||||
@ -103,8 +117,7 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
<Typography component='div' variant='body2'>
|
||||
<p>
|
||||
{t(
|
||||
'Maximum single trade size is {{maxAmount}} Satoshis to minimize lightning routing failure. There is no limits to the number of trades per day. A robot can only have one order at a time. However, you can use multiple robots simultaneously in different browsers (remember to back up your robot tokens!).',
|
||||
{ maxAmount: pn(maxAmount) },
|
||||
'Each RoboSats coordinator will set a maximum trade size to minimize the hassle of lightning routing failures. There is no limits to the number of trades per day. A robot can only have one order at a time. However, you can use multiple robots simultaneously using the Robot garage. Remember to back up your robot tokens!',
|
||||
)}{' '}
|
||||
</p>
|
||||
</Typography>
|
||||
@ -119,8 +132,7 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
<Typography component='div' variant='body2'>
|
||||
<p>
|
||||
{t(
|
||||
'RoboSats total fee for an order is {{tradeFee}}%. This fee is split to be covered by both: the order maker ({{makerFee}}%) and the order taker ({{takerFee}}%). In case an onchain address is used to received the Sats a variable swap fee applies. Check the exchange details by tapping on the bottom bar icon to see the current swap fee.',
|
||||
{ tradeFee: '0.2', makerFee: '0.025', takerFee: '0.175' },
|
||||
'The trade fee is collected by the robosats coordinator as a compensation for their service. You can see the fees of each coordinator by checking out their profile. The trade fee is split to be covered by both: the order maker and the order taker. Typically, the maker fee will be significantly smaller than the taker fee. In case an onchain address is used to received the Sats a variable swap fee applies. The onchain payout fee can also be seen in the profile of the coordinator.',
|
||||
)}{' '}
|
||||
</p>
|
||||
<p>
|
||||
@ -141,12 +153,24 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
'RoboSats will never ask you for your name, country or ID. RoboSats does not custody your funds and does not care who you are. RoboSats does not collect or custody any personal data. For best anonymity use Tor Browser and access the .onion hidden service.',
|
||||
'The RoboSats client, which you run on your local machine or browser, does not collect or share your IP address, location, name, or personal data. The client encrypts your private messages, which can only be decrypted by your trade partner.',
|
||||
)}{' '}
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
'The coordinator you choose will maintain a database of pseudonymous robots and orders for the application to function correctly. You can further enhance your privacy by using proxy nodes or coinjoining.',
|
||||
)}{' '}
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
'Your trade partner will not know the destination of the Lightning payment. The permanence of the data collected by the coordinators depend on their privacy and data policies. If a dispute arises, a coordinator may request additional information. The specifics of this process can vary from coordinator to coordinator.',
|
||||
)}{' '}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'Your trading peer is the only one who can potentially guess anything about you. Keep your chat short and concise. Avoid providing non-essential information other than strictly necessary for the fiat payment.',
|
||||
'During a typical order, your trading peer is the only one who can potentially guess anything about you. Keep your chat short and concise. Avoid providing non-essential information other than strictly necessary for the fiat payment.',
|
||||
)}{' '}
|
||||
</p>
|
||||
</Typography>
|
||||
@ -184,18 +208,20 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
"The buyer and the seller never have to trust each other. Some trust on RoboSats is needed since linking the seller's hold invoice and buyer payment is not atomic (yet). In addition, disputes are solved by the RoboSats staff.",
|
||||
"The buyer and the seller never have to trust each other. Some trust on the coordinator is needed since linking the seller's hold invoice and buyer payment is not atomic. In addition, disputes are solved by the coordinator. Make sure to select a coordinator with good reputation.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
"To be totally clear. Trust requirements are minimized. However, there is still one way RoboSats could run away with your satoshis: by not releasing the satoshis to the buyer. It could be argued that such move is not in RoboSats' interest as it would damage the reputation for a small payout. However, you should hesitate and only trade small quantities at a time. For large amounts use an onchain escrow service such as Bisq",
|
||||
"While trust requirements are minimized, there are ways for the coordinator to run away with your satoshis: for example, by not releasing the satoshis to the buyer. It could be argued that such move is not in the coordinator's interest as it would damage the reputation for a small payout. However, you should hesitate and only trade small quantities at a time. For large amounts you can use a high reputation DAO based escrow service such as Bisq",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
{t('You can build more trust on RoboSats by inspecting the source code.')}{' '}
|
||||
{t(
|
||||
'You can build more trust on the RoboSats and coordinator infrastructure by inspecting the source code.',
|
||||
)}{' '}
|
||||
<Link href='https://github.com/RoboSats/robosats'> {t('Project source code')}</Link>
|
||||
.{' '}
|
||||
</p>
|
||||
@ -205,14 +231,14 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
|
||||
<Accordion disableGutters={true}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>{t('What happens if RoboSats suddenly disappears?')}</Typography>
|
||||
<Typography>{t('What happens if my coordinator goes offline forever?')}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography component='div' variant='body2'>
|
||||
<p>
|
||||
{' '}
|
||||
{t(
|
||||
'Your sats will return to you. Any hold invoice that is not settled would be automatically returned even if RoboSats goes down forever. This is true for both, locked bonds and trading escrows. However, there is a small window between the seller confirms FIAT RECEIVED and the moment the buyer receives the satoshis when the funds could be permanently lost if RoboSats disappears. This window is about 1 second long. Make sure to have enough inbound liquidity to avoid routing failures. If you have any problem, reach out trough the RoboSats public channels.',
|
||||
'Your sats will return to you. Any hold invoice that is not settled would be automatically returned even if the coordinator goes down forever. This is true for both, locked bonds and trading escrows. However, there is a small window between the seller confirms FIAT RECEIVED and the moment the buyer receives the satoshis when the funds could be permanently lost if the coordinator disappears. This window is usually about 1 second long. Make sure to have enough inbound liquidity to avoid routing failures. If you have any problem, reach out trough the RoboSats public channels or directly to your trade coordinator using one of the contact methods listed on their profile.',
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
@ -248,7 +274,7 @@ const InfoDialog = ({ maxAmount, open, onClose }: Props): JSX.Element => {
|
||||
)}
|
||||
<Link href='https://t.me/robosats'>{t('(Telegram)')}</Link>
|
||||
{t(
|
||||
'. RoboSats will never contact you. RoboSats will definitely never ask for your robot token.',
|
||||
'. RoboSats developers will never contact you. The developers or the coordinators will definitely never ask for your robot token.',
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
@ -263,4 +289,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;
|
@ -15,10 +15,9 @@ import {
|
||||
} from '@mui/material';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import TwitterIcon from '@mui/icons-material/Twitter';
|
||||
import RedditIcon from '@mui/icons-material/Reddit';
|
||||
import Flags from 'country-flag-icons/react/3x2';
|
||||
import { NostrIcon, SimplexIcon } from '../Icons';
|
||||
import { NostrIcon, SimplexIcon, XIcon } from '../Icons';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@ -117,17 +116,14 @@ const CommunityDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
||||
<ListItemButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href='https://twitter.com/robosats'
|
||||
href='https://x.com/robosats'
|
||||
rel='noreferrer'
|
||||
>
|
||||
<ListItemIcon>
|
||||
<TwitterIcon color='primary' sx={{ height: 32, width: 32 }} />
|
||||
<XIcon color='primary' sx={{ height: 32, width: 32 }} />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={t('Follow RoboSats in Twitter')}
|
||||
secondary={t('Twitter Official Account')}
|
||||
/>
|
||||
<ListItemText primary={t('Follow RoboSats in X')} secondary={t('X Official Account')} />
|
||||
</ListItemButton>
|
||||
|
||||
<Divider />
|
||||
@ -156,7 +152,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 +163,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'
|
||||
|
851
frontend/src/components/Dialogs/Coordinator.tsx
Normal file
851
frontend/src/components/Dialogs/Coordinator.tsx
Normal file
@ -0,0 +1,851 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
Alert,
|
||||
DialogContent,
|
||||
Divider,
|
||||
Grid,
|
||||
List,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Link,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
AlertTitle,
|
||||
ListItemButton,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Inventory,
|
||||
Sell,
|
||||
SmartToy,
|
||||
Percent,
|
||||
PriceChange,
|
||||
Book,
|
||||
Reddit,
|
||||
Key,
|
||||
Bolt,
|
||||
Description,
|
||||
Dns,
|
||||
Email,
|
||||
Equalizer,
|
||||
ExpandMore,
|
||||
GitHub,
|
||||
Language,
|
||||
Send,
|
||||
Tag,
|
||||
Web,
|
||||
VolunteerActivism,
|
||||
Circle,
|
||||
Flag,
|
||||
} from '@mui/icons-material';
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
|
||||
import { pn } from '../../utils';
|
||||
import { type Contact } from '../../models';
|
||||
import RobotAvatar from '../RobotAvatar';
|
||||
import {
|
||||
AmbossIcon,
|
||||
BitcoinSignIcon,
|
||||
RoboSatsNoTextIcon,
|
||||
BadgeFounder,
|
||||
BadgeDevFund,
|
||||
BadgePrivacy,
|
||||
BadgeLoved,
|
||||
BadgeLimits,
|
||||
NostrIcon,
|
||||
SimplexIcon,
|
||||
XIcon,
|
||||
} from '../Icons';
|
||||
import { AppContext } from '../../contexts/AppContext';
|
||||
import { systemClient } from '../../services/System';
|
||||
import { type Badges } from '../../models/Coordinator.model';
|
||||
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
shortAlias: string | null;
|
||||
network: 'mainnet' | 'testnet' | undefined;
|
||||
}
|
||||
|
||||
const ContactButtons = ({
|
||||
nostr,
|
||||
pgp,
|
||||
fingerprint,
|
||||
email,
|
||||
telegram,
|
||||
twitter,
|
||||
matrix,
|
||||
simplex,
|
||||
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('Download PGP Pubkey. Fingerprint: ') + fingerprint.match(/.{1,4}/g).join(' ')}
|
||||
>
|
||||
<IconButton component='a' target='_blank' href={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('X')}>
|
||||
<IconButton
|
||||
component='a'
|
||||
target='_blank'
|
||||
href={`https://x.com/${twitter}`}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<XIcon sx={{ width: '0.8em', height: '0.8em' }} />
|
||||
</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>
|
||||
)}
|
||||
|
||||
{simplex !== undefined && (
|
||||
<Grid item>
|
||||
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Simplex')}>
|
||||
<IconButton component='a' target='_blank' href={`${simplex}`} rel='noreferrer'>
|
||||
<SimplexIcon sx={{ width: '0.7em', height: '0.7em' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
interface BadgesProps {
|
||||
badges: Badges | undefined;
|
||||
size_limit: number | undefined;
|
||||
}
|
||||
|
||||
const BadgesHall = ({ badges, size_limit }: 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'>
|
||||
{size_limit > 3000000
|
||||
? t('Large limits: the coordinator has large trade limits.')
|
||||
: t('Does not have large trade limits.')}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Grid item sx={{ filter: size_limit > 3000000 ? undefined : 'grayscale(100%)' }}>
|
||||
<BadgeLimits sx={sxProps} />
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { clientVersion, page, settings, origin } = useContext(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const coordinator = federation.getCoordinator(shortAlias);
|
||||
|
||||
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
|
||||
shortAlias={coordinator?.shortAlias}
|
||||
style={{ width: '7.5em', height: '7.5em' }}
|
||||
smooth={true}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{Boolean(coordinator?.info?.notice_severity) &&
|
||||
coordinator?.info?.notice_severity !== 'none' && (
|
||||
<ListItem>
|
||||
<Alert severity={coordinator?.info?.notice_severity} sx={{ width: '100%' }}>
|
||||
<AlertTitle>{t('Coordinator Notice')}</AlertTitle>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: coordinator?.info?.notice_message ?? '' }}
|
||||
/>
|
||||
</Alert>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem>
|
||||
<BadgesHall badges={coordinator?.badges} size_limit={coordinator?.size_limit} />
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Description />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.description}
|
||||
primaryTypographyProps={{ sx: { maxWidth: '20em' } }}
|
||||
secondary={t('Coordinator description')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Flag />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={coordinator?.established.toLocaleDateString(settings.language)}
|
||||
secondary={t('Established')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItemButton
|
||||
target='_blank'
|
||||
href={coordinator?.[settings.network][settings.selfhostedClient ? 'onion' : origin]}
|
||||
rel='noreferrer'
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Web />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
secondary={t('Coordinator hosted web app')}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
maxWidth: '20em',
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{`${String(
|
||||
coordinator?.[settings.network][settings.selfhostedClient ? 'onion' : origin],
|
||||
)}`}
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
</List>
|
||||
|
||||
{coordinator?.loadingInfo ? (
|
||||
<Box style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : coordinator?.info ? (
|
||||
<Box>
|
||||
{Boolean(coordinator?.policies) && (
|
||||
<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>
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<Circle />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${pn(
|
||||
Math.min(coordinator?.size_limit, coordinator?.info?.max_order_size),
|
||||
)} Sats`}
|
||||
secondary={t('Maximum order size')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<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 />
|
||||
|
||||
{!coordinator?.info?.swap_enabled ? (
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={t('Onchain payouts disabled')}
|
||||
primaryTypographyProps={{ color: 'red' }}
|
||||
secondary={t('Current onchain payout status')}
|
||||
/>
|
||||
</ListItem>
|
||||
) : (
|
||||
<>
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${coordinator?.info?.current_swap_fee_rate.toPrecision(3)}%`}
|
||||
secondary={t('Current onchain payout fee')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon />
|
||||
|
||||
<ListItemText
|
||||
primary={`${pn(
|
||||
Math.min(
|
||||
coordinator?.size_limit,
|
||||
coordinator?.info?.max_order_size,
|
||||
coordinator?.info?.max_swap,
|
||||
),
|
||||
)} Sats`}
|
||||
secondary={t('Maximum onchain swap size')}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem {...listItemProps}>
|
||||
<ListItemIcon>
|
||||
<VolunteerActivism />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={`${coordinator?.badges?.donatesToDevFund}% of profits`}
|
||||
secondary={t('Zaps voluntarily for development')}
|
||||
/>
|
||||
</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: '0.6em', height: '0.6em' }}
|
||||
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: '0.6em', height: '0.6em' }}
|
||||
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}`);
|
||||
};
|
||||
|
||||
|
193
frontend/src/components/Dialogs/Exchange.tsx
Normal file
193
frontend/src/components/Dialogs/Exchange.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } 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, coordinatorUpdatedAt, federationUpdatedAt } = useContext(FederationContext);
|
||||
const [loadingProgress, setLoadingProgress] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const loadedCoordinators =
|
||||
federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators;
|
||||
setLoadingProgress((loadedCoordinators / federation.exchange.enabledCoordinators) * 100);
|
||||
}, [open, coordinatorUpdatedAt, federationUpdatedAt]);
|
||||
|
||||
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.enabledCoordinators}
|
||||
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.toFixed(8))}
|
||||
<BitcoinSignIcon sx={{ width: '0.6em', height: '0.6em' }} 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.toFixed(8))}
|
||||
<BitcoinSignIcon sx={{ width: '0.6em', height: '0.6em' }} 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,14 +41,14 @@ 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] != null && position?.[1] != null) {
|
||||
onClose([position[0] + Math.random() * 0.1 - 0.05, position[1] + Math.random() * 0.1 - 0.05]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open && latitude && longitude) {
|
||||
if (open && latitude != null && longitude != null) {
|
||||
setPosition([latitude, longitude]);
|
||||
} else {
|
||||
setPosition(undefined);
|
||||
@ -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,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Dialog, Alert, AlertTitle } from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
severity: 'warning' | 'success' | 'error' | 'info' | 'none';
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const NoticeDialog = ({ open = false, severity, message, onClose }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<Alert severity={severity !== 'none' ? severity : 'info'}>
|
||||
<AlertTitle>{t('Coordinator Notice')}</AlertTitle>
|
||||
<div dangerouslySetInnerHTML={{ __html: message }} />
|
||||
</Alert>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeDialog;
|
@ -1,126 +1,48 @@
|
||||
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, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { type 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, 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 slot = garage.getSlot();
|
||||
|
||||
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);
|
||||
const [loadingCoordinators, setLoadingCoordinators] = useState<number>(
|
||||
Object.values(slot?.robots ?? {}).length,
|
||||
);
|
||||
|
||||
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.getSlot()?.hashId);
|
||||
setLoadingCoordinators(
|
||||
Object.values(slot?.robots ?? {}).filter((robot) => robot.loading).length,
|
||||
);
|
||||
}, [robotUpdatedAt]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -129,21 +51,20 @@ 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>
|
||||
<Typography component='h5' variant='h5'>
|
||||
{t('Your Robot')}
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
<Divider />
|
||||
|
||||
<ListItem className='profileNickname'>
|
||||
<ListItemText secondary={t('Your robot')}>
|
||||
<ListItemText>
|
||||
<Typography component='h6' variant='h6'>
|
||||
{robot.nickname ? (
|
||||
{garage.getSlot()?.nickname !== undefined && (
|
||||
<div style={{ position: 'relative', left: '-7px' }}>
|
||||
<div
|
||||
style={{
|
||||
@ -156,232 +77,52 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
>
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
|
||||
<a>{robot.nickname}</a>
|
||||
<a>{garage.getSlot()?.nickname}</a>
|
||||
|
||||
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
{loadingCoordinators > 0 ? (
|
||||
<>
|
||||
<b>{t('Looking for your robot!')}</b>
|
||||
<LinearProgress />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ListItemText>
|
||||
|
||||
<ListItemAvatar>
|
||||
<RobotAvatar
|
||||
avatarClass='profileAvatar'
|
||||
style={{ width: 65, height: 65 }}
|
||||
nickname={robot.nickname}
|
||||
baseUrl={baseUrl}
|
||||
hashId={garage.getSlot()?.hashId ?? ''}
|
||||
/>
|
||||
</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 robot:')}</b>
|
||||
</Typography>
|
||||
|
||||
{Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => {
|
||||
const coordinatorRobot = garage.getSlot()?.getRobot(coordinator.shortAlias);
|
||||
return (
|
||||
<div key={coordinator.shortAlias}>
|
||||
<RobotInfo
|
||||
coordinator={coordinator}
|
||||
onClose={onClose}
|
||||
disabled={coordinatorRobot?.loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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.getSlot()?.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.getSlot()?.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;
|
41
frontend/src/components/Dialogs/Warning.tsx
Normal file
41
frontend/src/components/Dialogs/Warning.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
longAlias: string;
|
||||
}
|
||||
|
||||
const WarningDialog = ({ open, onClose, longAlias }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t('Warning')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t(
|
||||
'Coordinators of p2p trades are the source of trust, provide the infrastructure, pricing and will mediate in case of dispute. Make sure you research and trust "{{coordinator_name}}" before locking your bond. A malicious p2p coordinator can find ways to steal from you.',
|
||||
{ coordinator_name: longAlias },
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('I understand')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarningDialog;
|
@ -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';
|
||||
export { default as WarningDialog } from './Warning';
|
||||
|
@ -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' }}>
|
||||
|
247
frontend/src/components/FederationTable/index.tsx
Normal file
247
frontend/src/components/FederationTable/index.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
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, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface FederationTableProps {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
fillContainer?: boolean;
|
||||
}
|
||||
|
||||
const FederationTable = ({
|
||||
maxWidth = 90,
|
||||
maxHeight = 50,
|
||||
fillContainer = false,
|
||||
}: FederationTableProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { federation, sortedCoordinators, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { setOpen } = useContext<UseAppStoreType>(AppContext);
|
||||
const theme = useTheme();
|
||||
const [pageSize, setPageSize] = useState<number>(0);
|
||||
|
||||
// all sizes in 'em'
|
||||
const fontSize = theme.typography.fontSize;
|
||||
const verticalHeightFrame = 3.3;
|
||||
const verticalHeightRow = 3.27;
|
||||
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);
|
||||
}
|
||||
}, [coordinatorUpdatedAt]);
|
||||
|
||||
const localeText = {
|
||||
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
|
||||
noResultsOverlayLabel: t('No coordinators found.'),
|
||||
};
|
||||
|
||||
const onClickCoordinator = function (shortAlias: string): void {
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: shortAlias };
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
shortAlias={params.row.shortAlias}
|
||||
style={{ width: '3.215em', height: '3.215em' }}
|
||||
smooth={true}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[coordinatorUpdatedAt],
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[coordinatorUpdatedAt],
|
||||
);
|
||||
|
||||
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 === true) {
|
||||
federation.disableCoordinator(shortAlias);
|
||||
} else {
|
||||
federation.enableCoordinator(shortAlias);
|
||||
}
|
||||
};
|
||||
|
||||
const reorderedCoordinators = sortedCoordinators.reduce((coordinators, key) => {
|
||||
coordinators[key] = federation.coordinators[key];
|
||||
return coordinators;
|
||||
}, {});
|
||||
|
||||
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(reorderedCoordinators)}
|
||||
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();
|
||||
|
@ -3,6 +3,33 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { Paper, Alert, AlertTitle, Button, Link } from '@mui/material';
|
||||
import { getHost } from '../../utils';
|
||||
import defaultFederation from '../../../static/federation.json';
|
||||
|
||||
function federationUrls(): string[] {
|
||||
const urls: string[] = [];
|
||||
|
||||
const removeProtocol = (url: string): string => {
|
||||
return url.replace(/^https?:\/\/|\/\/$/, '');
|
||||
};
|
||||
|
||||
for (const key in defaultFederation) {
|
||||
const mainnet = defaultFederation[key].mainnet;
|
||||
const testnet = defaultFederation[key].testnet;
|
||||
|
||||
// Add the URLs from the 'mainnet' and 'testnet' objects to the urls array
|
||||
// if these are onion or i2p addresses
|
||||
for (const safeOrigin of ['onion', 'i2p']) {
|
||||
if (mainnet?.[safeOrigin]) urls.push(removeProtocol(mainnet[safeOrigin]));
|
||||
if (testnet?.[safeOrigin]) urls.push(removeProtocol(testnet[safeOrigin]));
|
||||
}
|
||||
}
|
||||
|
||||
// web hosted frontend without coordinator
|
||||
urls.push('robodexarjwtfryec556cjdz3dfa7u47saek6lkftnkgshvgg2kcumqd.onion');
|
||||
return urls;
|
||||
}
|
||||
|
||||
export const safeUrls = federationUrls();
|
||||
|
||||
const UnsafeAlert = (): JSX.Element => {
|
||||
const { windowSize } = useContext<UseAppStoreType>(AppContext);
|
||||
@ -11,20 +38,8 @@ const UnsafeAlert = (): JSX.Element => {
|
||||
|
||||
const [unsafeClient, setUnsafeClient] = useState<boolean>(false);
|
||||
|
||||
// To do. Read from Coordinators Obj.
|
||||
const safe_urls = [
|
||||
'robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion',
|
||||
'robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion',
|
||||
'robodevs7ixniseezbv7uryxhamtz3hvcelzfwpx3rvoipttjomrmpqd.onion',
|
||||
'robosats.i2p',
|
||||
'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);
|
||||
};
|
||||
|
||||
@ -41,71 +56,42 @@ const UnsafeAlert = (): JSX.Element => {
|
||||
else if (unsafeClient) {
|
||||
return (
|
||||
<Paper elevation={6} className='unsafeAlert'>
|
||||
{windowSize.width > 57 ? (
|
||||
<Alert
|
||||
severity='warning'
|
||||
sx={{ maxHeight: '7em' }}
|
||||
action={
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
{t('Hide')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<AlertTitle>{t('You are not using RoboSats privately')}</AlertTitle>
|
||||
<Trans i18nKey='desktop_unsafe_alert'>
|
||||
<a>
|
||||
Some features are disabled for your protection (e.g. chat) and you will not be able
|
||||
to complete a trade without them. To protect your privacy and fully enable RoboSats,
|
||||
use{' '}
|
||||
</a>
|
||||
<Link href='https://www.torproject.org/download/' target='_blank'>
|
||||
Tor Browser
|
||||
</Link>
|
||||
<a> and visit the </a>
|
||||
<Link
|
||||
href='http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
|
||||
target='_blank'
|
||||
>
|
||||
Onion
|
||||
</Link>
|
||||
<a> site.</a>
|
||||
</Trans>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity='warning' sx={{ maxHeight: '8em' }}>
|
||||
<AlertTitle>{t('You are not using RoboSats privately')}</AlertTitle>
|
||||
<Trans i18nKey='phone_unsafe_alert'>
|
||||
<a>You will not be able to complete a trade. Use </a>
|
||||
<Link href='https://www.torproject.org/download/' target='_blank'>
|
||||
Tor Browser
|
||||
</Link>
|
||||
<a> and visit the </a>
|
||||
<Link
|
||||
href='http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
|
||||
target='_blank'
|
||||
>
|
||||
Onion
|
||||
</Link>{' '}
|
||||
<a> site.</a>
|
||||
</Trans>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Button
|
||||
className='hideAlertButton'
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
{t('Hide')}
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
<Alert
|
||||
severity='warning'
|
||||
sx={{ maxHeight: windowSize?.width > 57 ? '7em' : '8em' }}
|
||||
action={
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
}}
|
||||
>
|
||||
{t('Hide')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<AlertTitle>{t('You are not using RoboSats privately')}</AlertTitle>
|
||||
<Trans i18nKey='unsafe_alert'>
|
||||
<a>To fully enable RoboSats and protect your data and privacy, use </a>
|
||||
<Link href='https://www.torproject.org/download/' target='_blank'>
|
||||
Tor Browser
|
||||
</Link>
|
||||
<a> and visit the federation hosted </a>
|
||||
<Link
|
||||
href='http://robodexarjwtfryec556cjdz3dfa7u47saek6lkftnkgshvgg2kcumqd.onion'
|
||||
target='_blank'
|
||||
>
|
||||
<b>Onion</b>
|
||||
</Link>
|
||||
<a> site or </a>
|
||||
<Link href='https://apps.umbrel.com/app/robosats' target='_blank'>
|
||||
host your own app.
|
||||
</Link>
|
||||
</Trans>
|
||||
</Alert>
|
||||
</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;
|
||||
|
15
frontend/src/components/Icons/X.tsx
Normal file
15
frontend/src/components/Icons/X.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { SvgIcon, type SvgIconProps } from '@mui/material';
|
||||
|
||||
const X: React.FC<SvgIconProps> = (props) => {
|
||||
return (
|
||||
<SvgIcon sx={props.sx} color={props.color} x='0px' y='0px' viewBox='0 0 1200 1227'>
|
||||
<path
|
||||
d='M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z'
|
||||
fill={props.color}
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default X;
|
@ -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,14 @@ 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';
|
||||
export { default as XIcon } from './X';
|
||||
|
||||
// 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,17 @@ 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,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { fiatMethods, swapMethods, PaymentIcon } from '../PaymentMethods';
|
||||
|
||||
// Icons
|
||||
@ -20,12 +30,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 +51,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 +74,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 +86,10 @@ const InputWrapper = styled('div')(
|
||||
&.focused {
|
||||
border: 2px solid ${
|
||||
theme.palette.mode === 'dark'
|
||||
? error
|
||||
? error === true
|
||||
? '#f44336'
|
||||
: '#90caf9'
|
||||
: error
|
||||
: error === true
|
||||
? '#dd0000'
|
||||
: '#1976d2'
|
||||
};
|
||||
@ -93,31 +115,63 @@ const InputWrapper = styled('div')(
|
||||
|
||||
interface TagProps {
|
||||
label: string;
|
||||
icon: string;
|
||||
onDelete: () => void;
|
||||
icon?: string;
|
||||
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 (
|
||||
<div {...other}>
|
||||
<div style={{ position: 'relative', left: '-5px', top: '0.28em' }} onClick={onClick}>
|
||||
<PaymentIcon width={iconSize} height={iconSize} icon={icon} />
|
||||
</div>
|
||||
{icon ? (
|
||||
<div style={{ position: 'relative', left: '-5px', top: '0.28em' }} onClick={onClick}>
|
||||
<PaymentIcon width={iconSize} height={iconSize} icon={icon} />
|
||||
</div>
|
||||
) : null}
|
||||
<span style={{ position: 'relative', left: '2px' }} onClick={onClick}>
|
||||
{label}
|
||||
</span>
|
||||
<CloseIcon onClick={onDelete} />
|
||||
<CloseIcon className='delete-icon' onClick={onDelete} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledChip = styled(Chip)(
|
||||
({ theme, sx }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${String(sx?.height ?? '1.6rem')};
|
||||
margin: 2px;
|
||||
line-height: 1.5em;
|
||||
background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'};
|
||||
border: 1px solid ${theme.palette.mode === 'dark' ? '#303030' : '#e8e8e8'};
|
||||
border-radius: 2px;
|
||||
box-sizing: content-box;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'};
|
||||
background-color: ${theme.palette.mode === 'dark' ? '#003b57' : '#e6f7ff'};
|
||||
}
|
||||
|
||||
& span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.928em;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledTag = styled(Tag)(
|
||||
({ theme, sx }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${sx.height};
|
||||
height: ${String(sx?.height ?? '1.6rem')};
|
||||
margin: 2px;
|
||||
line-height: 1.5em;
|
||||
background-color: ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'};
|
||||
@ -163,7 +217,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,8 +263,32 @@ 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;
|
||||
multiple: number;
|
||||
optionsDisplayLimit?: number;
|
||||
listHeaderText: string;
|
||||
}
|
||||
|
||||
const AutocompletePayments: React.FC<AutocompletePaymentsProps> = (props) => {
|
||||
// ** State
|
||||
const [val, setVal] = useState('');
|
||||
const [showFilterInput, setShowFilterInput] = useState(false);
|
||||
|
||||
// ** Hooks
|
||||
const { t } = useTranslation();
|
||||
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const {
|
||||
getRootProps,
|
||||
getInputLabelProps,
|
||||
@ -220,59 +298,79 @@ export default function AutocompletePayments(props) {
|
||||
getOptionProps,
|
||||
groupedOptions,
|
||||
value,
|
||||
focused = 'true',
|
||||
popupOpen,
|
||||
setAnchorEl,
|
||||
} = useAutocomplete({
|
||||
fullWidth: true,
|
||||
id: 'payment-methods',
|
||||
multiple: true,
|
||||
value: props.value,
|
||||
options: props.optionsType == 'fiat' ? fiatMethods : swapMethods,
|
||||
options: props.optionsType === 'fiat' ? fiatMethods : swapMethods,
|
||||
open: props.isFilter ? showFilterInput : undefined,
|
||||
getOptionLabel: (option) => option.name,
|
||||
onInputChange: (e) => {
|
||||
setVal(e ? (e.target.value ? e.target.value : '') : '');
|
||||
if (e?.target) setVal(e?.target?.value ?? '');
|
||||
},
|
||||
onChange: (event, value) => {
|
||||
if (props.isFilter) setShowFilterInput(false);
|
||||
props.onAutocompleteChange(value);
|
||||
},
|
||||
onOpen: () => {
|
||||
if (props.isFilter) setShowFilterInput(true);
|
||||
},
|
||||
onChange: (event, value) => props.onAutocompleteChange(value),
|
||||
onClose: () => {
|
||||
setVal(() => '');
|
||||
},
|
||||
});
|
||||
|
||||
const [val, setVal] = useState('');
|
||||
const fewerOptions = groupedOptions.length > 8 ? groupedOptions.slice(0, 8) : groupedOptions;
|
||||
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;
|
||||
}
|
||||
|
||||
const tagsToDisplay = value.length
|
||||
? props.optionsDisplayLimit
|
||||
? value.slice(0, props.optionsDisplayLimit)
|
||||
: value
|
||||
: [];
|
||||
|
||||
const qttHiddenTags = props.optionsDisplayLimit ? value.length - props.optionsDisplayLimit : 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showFilterInput && props.isFilter) {
|
||||
filterInputRef.current?.focus();
|
||||
document.getElementById('payment-methods')?.focus();
|
||||
}
|
||||
}, [showFilterInput]);
|
||||
|
||||
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,31 +378,71 @@ export default function AutocompletePayments(props) {
|
||||
</Fade>
|
||||
<InputWrapper
|
||||
ref={setAnchorEl}
|
||||
error={props.error ? 'error' : null}
|
||||
className={focused ? 'focused' : ''}
|
||||
error={Boolean(props.error)}
|
||||
className={popupOpen ? 'focused' : ''}
|
||||
sx={{
|
||||
minHeight: '2.9em',
|
||||
maxHeight: '8.6em',
|
||||
hoverBorderColor: '#ffffff',
|
||||
...props.sx,
|
||||
}}
|
||||
onClick={(evt) => {
|
||||
if (props.isFilter) {
|
||||
if (evt.target instanceof HTMLElement && !evt.target.matches('.delete-icon')) {
|
||||
// Check if click is not on delete
|
||||
setShowFilterInput(!showFilterInput);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value.map((option, index) => (
|
||||
<StyledTag
|
||||
label={t(option.name)}
|
||||
icon={option.icon}
|
||||
sx={{ height: '2.1em', ...(props.tagProps ? props.tagProps.sx : {}) }}
|
||||
onClick={props.onClick}
|
||||
{...getTagProps({ index })}
|
||||
{!showFilterInput || !props.isFilter ? (
|
||||
<>
|
||||
{tagsToDisplay.map((option, index) => (
|
||||
<StyledTag
|
||||
key={index}
|
||||
label={t(option.name)}
|
||||
icon={option.icon}
|
||||
onClick={props.onClick}
|
||||
sx={{ height: '1.6rem', ...(props.tagProps ?? {}) }}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))}
|
||||
{qttHiddenTags > 0 ? (
|
||||
<StyledChip
|
||||
sx={{ borderRadius: 1 }}
|
||||
label={`+${qttHiddenTags}`}
|
||||
sx={{ height: '1.6rem' }}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{value.length > 0 && !props.multiple ? null : (
|
||||
<input
|
||||
ref={filterInputRef}
|
||||
autoFocus={true}
|
||||
style={
|
||||
props.isFilter
|
||||
? {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'transparent',
|
||||
display: showFilterInput ? 'block' : 'none',
|
||||
width: '166px',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
{...getInputProps()}
|
||||
value={val}
|
||||
onBlur={() => {
|
||||
if (props.isFilter) setShowFilterInput(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{value.length > 0 && props.isFilter ? null : <input {...getInputProps()} value={val} />}
|
||||
)}
|
||||
</InputWrapper>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Grow in={fewerOptions.length > 0}>
|
||||
<Listbox sx={props.listBoxProps ? props.listBoxProps.sx : undefined} {...getListboxProps()}>
|
||||
{!props.isFilter ? (
|
||||
<Listbox sx={props.listBoxProps?.sx ?? undefined} {...getListboxProps()}>
|
||||
{props.listHeaderText ? (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
@ -341,7 +479,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 +497,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 +510,6 @@ export default function AutocompletePayments(props) {
|
||||
</Grow>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AutocompletePayments;
|
||||
|
@ -2,7 +2,6 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
InputAdornment,
|
||||
LinearProgress,
|
||||
ButtonGroup,
|
||||
Slider,
|
||||
Switch,
|
||||
@ -40,8 +39,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, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
|
||||
interface MakerFormProps {
|
||||
disableRequest?: boolean;
|
||||
@ -50,7 +52,7 @@ interface MakerFormProps {
|
||||
onSubmit?: () => void;
|
||||
onReset?: () => void;
|
||||
submitButtonLabel?: string;
|
||||
onOrderCreated?: (id: number) => void;
|
||||
onOrderCreated?: (shortAlias: string, id: number) => void;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
@ -64,8 +66,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, coordinatorUpdatedAt, federationUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { maker, setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@ -80,57 +84,84 @@ 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);
|
||||
// Why?
|
||||
// const slot = garage.getSlot();
|
||||
// if (slot?.token) void federation.fetchRobot(garage, slot?.token);
|
||||
}, [garage.currentSlot]);
|
||||
|
||||
fetchLimits();
|
||||
useEffect(() => {
|
||||
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
|
||||
}, [coordinatorUpdatedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
updateCoordinatorInfo();
|
||||
}, [maker.coordinator, coordinatorUpdatedAt]);
|
||||
|
||||
const updateCoordinatorInfo = (): void => {
|
||||
if (maker.coordinator != null) {
|
||||
const newLimits = federation.getCoordinator(maker.coordinator).limits;
|
||||
if (Object.keys(newLimits).length !== 0) {
|
||||
updateAmountLimits(newLimits, fav.currency, maker.premium);
|
||||
updateCurrentPrice(newLimits, fav.currency, maker.premium);
|
||||
updateSatoshisLimits(newLimits);
|
||||
setLimits(newLimits);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const coordinatorSizeLimit =
|
||||
(federation.getCoordinator(maker.coordinator).size_limit / 100000000) *
|
||||
limitList[index].price;
|
||||
maxAmountLimit = Math.min(coordinatorSizeLimit, maxAmountLimit);
|
||||
|
||||
// apply thresholds to ensure good request
|
||||
minAmountLimit = minAmountLimit * amountSafeThresholds[0];
|
||||
maxAmountLimit = maxAmountLimit * amountSafeThresholds[1];
|
||||
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;
|
||||
let maxAmount: number = limitList[1000].max_amount * 100000000;
|
||||
maxAmount = Math.min(
|
||||
federation.getCoordinator(maker.coordinator).size_limit / 100000000,
|
||||
maxAmount,
|
||||
);
|
||||
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 +169,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 +194,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 +223,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 +242,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 +255,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 +264,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 +284,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 +300,26 @@ const MakerForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateOrder = function () {
|
||||
if (!disableRequest) {
|
||||
const handleCreateOrder = function (): void {
|
||||
const slot = garage.getSlot();
|
||||
|
||||
if (slot?.activeShortAlias) {
|
||||
setBadRequest(t('You are already maker of an active order'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, basePath } =
|
||||
federation
|
||||
.getCoordinator(maker.coordinator)
|
||||
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl) ?? {};
|
||||
|
||||
const auth = slot?.getRobot()?.getAuthHeaders();
|
||||
|
||||
if (!disableRequest && maker.coordinator != null && auth !== null) {
|
||||
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 +327,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 +335,54 @@ const MakerForm = ({
|
||||
latitude: maker.latitude,
|
||||
longitude: maker.longitude,
|
||||
};
|
||||
|
||||
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);
|
||||
garage.updateOrder(data);
|
||||
}
|
||||
setSubmittingRequest(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setBadRequest('Request error');
|
||||
setSubmittingRequest(false);
|
||||
});
|
||||
}
|
||||
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 +409,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
|
||||
? parseFloat((maker.amount / 2).toPrecision(2))
|
||||
: parseFloat(Number(limits.list[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));
|
||||
const minAmount =
|
||||
maker.amount !== ''
|
||||
? parseFloat((maker.amount / 2).toPrecision(2))
|
||||
: parseFloat(Number(limits[index].max_amount * 0.25).toPrecision(2));
|
||||
const maxAmount =
|
||||
maker.amount !== ''
|
||||
? parseFloat(maker.amount)
|
||||
: parseFloat(Number(limits[index].max_amount * 0.75).toPrecision(2));
|
||||
|
||||
setMaker({
|
||||
...maker,
|
||||
@ -369,7 +428,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 +465,14 @@ const MakerForm = ({
|
||||
const handleClickAmountRangeEnabled = function (
|
||||
_e: React.ChangeEvent<HTMLInputElement>,
|
||||
checked: boolean,
|
||||
) {
|
||||
): void {
|
||||
setAmountRangeEnabled(checked);
|
||||
};
|
||||
|
||||
const amountLabel = useMemo(() => {
|
||||
if (!(maker.coordinator != null)) return;
|
||||
|
||||
const info = federation.getCoordinator(maker.coordinator)?.info;
|
||||
const defaultRoutingBudget = 0.001;
|
||||
let label = t('Amount');
|
||||
let helper = '';
|
||||
@ -420,7 +482,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 +493,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 +502,32 @@ 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 !== '') ||
|
||||
federation.getCoordinator(maker.coordinator)?.info === undefined ||
|
||||
federation.getCoordinator(maker.coordinator)?.limits === undefined
|
||||
);
|
||||
}, [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 +535,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 +547,7 @@ const MakerForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const SummaryText = function () {
|
||||
const SummaryText = (): JSX.Element => {
|
||||
return (
|
||||
<Typography
|
||||
component='h2'
|
||||
@ -494,7 +559,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 +577,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 +596,7 @@ const MakerForm = ({
|
||||
setOpenDialogs(false);
|
||||
}}
|
||||
onClickDone={handleCreateOrder}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
hasRobot={Boolean(garage.getSlot()?.hashId)}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
<F2fMapDialog
|
||||
@ -542,19 +607,14 @@ 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}
|
||||
zoom={maker.latitude != null && maker.longitude != null ? 6 : undefined}
|
||||
/>
|
||||
<Collapse in={limits.list.length == 0}>
|
||||
<div style={{ display: limits.list.length == 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 +650,7 @@ const MakerForm = ({
|
||||
>
|
||||
<Switch
|
||||
size='small'
|
||||
disabled={limits.list.length == 0}
|
||||
disabled={Object.keys(limits).length === 0}
|
||||
checked={maker.advancedOptions}
|
||||
onChange={handleClickAdvanced}
|
||||
/>
|
||||
@ -611,9 +671,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 +696,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 +716,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 +790,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 +821,7 @@ const MakerForm = ({
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{fav.mode === 'swap' && maker.amount != '' ? (
|
||||
{fav.mode === 'swap' && maker.amount !== '' ? (
|
||||
<FormHelperText sx={{ textAlign: 'center' }}>
|
||||
{amountLabel.helper}
|
||||
</FormHelperText>
|
||||
@ -780,7 +840,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,19 +866,22 @@ 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.',
|
||||
)}
|
||||
listHeaderText={t('You can add new methods')}
|
||||
addNewButtonText={t('Add New')}
|
||||
asFilter={false}
|
||||
isFilter={false}
|
||||
multiple={true}
|
||||
value={maker.paymentMethods}
|
||||
/>
|
||||
{maker.badPaymentMethod && (
|
||||
@ -844,7 +907,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 +978,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 +998,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 +1161,15 @@ const MakerForm = ({
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
<SelectCoordinator
|
||||
coordinatorAlias={maker.coordinator}
|
||||
setCoordinator={(coordinatorAlias) => {
|
||||
setMaker((maker) => {
|
||||
return { ...maker, coordinator: coordinatorAlias };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Grid container direction='column' alignItems='center'>
|
||||
<Grid item>
|
||||
<SummaryText />
|
||||
@ -1152,7 +1226,7 @@ const MakerForm = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Collapse in={!(limits.list.length == 0)}>
|
||||
<Collapse in={!(Object.keys(limits).length === 0)}>
|
||||
<Tooltip
|
||||
placement='top'
|
||||
enterTouchDelay={0}
|
||||
|
136
frontend/src/components/MakerForm/SelectCoordinator.tsx
Normal file
136
frontend/src/components/MakerForm/SelectCoordinator.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
Tooltip,
|
||||
Typography,
|
||||
type SelectChangeEvent,
|
||||
CircularProgress,
|
||||
} 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, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface SelectCoordinatorProps {
|
||||
coordinatorAlias: string;
|
||||
setCoordinator: (coordinatorAlias: string) => void;
|
||||
}
|
||||
|
||||
const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
|
||||
coordinatorAlias,
|
||||
setCoordinator,
|
||||
}) => {
|
||||
const { setOpen } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, sortedCoordinators, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClickCurrentCoordinator = function (shortAlias: string): void {
|
||||
setOpen((open) => {
|
||||
return { ...open, coordinator: shortAlias };
|
||||
});
|
||||
};
|
||||
|
||||
const handleCoordinatorChange = (e: SelectChangeEvent<string>): void => {
|
||||
setCoordinator(e.target.value);
|
||||
};
|
||||
|
||||
const coordinator = useMemo(
|
||||
() => federation.getCoordinator(coordinatorAlias),
|
||||
[coordinatorUpdatedAt],
|
||||
);
|
||||
|
||||
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(coordinatorAlias);
|
||||
}}
|
||||
>
|
||||
<Grid item>
|
||||
<RobotAvatar
|
||||
shortAlias={coordinatorAlias}
|
||||
style={{ width: '3em', height: '3em' }}
|
||||
smooth={true}
|
||||
flipHorizontally={false}
|
||||
small={true}
|
||||
/>
|
||||
{(coordinator?.info === undefined ||
|
||||
Object.keys(coordinator?.limits).length === 0) && (
|
||||
<CircularProgress
|
||||
size={49}
|
||||
thickness={5}
|
||||
style={{ marginTop: -48, position: 'absolute' }}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={9}>
|
||||
<Select
|
||||
variant='standard'
|
||||
fullWidth
|
||||
required={true}
|
||||
inputProps={{
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
value={coordinatorAlias}
|
||||
onChange={handleCoordinatorChange}
|
||||
disableUnderline
|
||||
>
|
||||
{sortedCoordinators.map((shortAlias: string): JSX.Element | null => {
|
||||
let row: JSX.Element | null = null;
|
||||
const item = federation.getCoordinator(shortAlias);
|
||||
if (item.enabled === true) {
|
||||
row = (
|
||||
<MenuItem key={shortAlias} value={shortAlias}>
|
||||
<Typography>{item.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, type 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>`;
|
||||
};
|
||||
|
||||
@ -27,7 +26,7 @@ interface Props {
|
||||
position?: [number, number] | undefined;
|
||||
setPosition?: (position: [number, number]) => void;
|
||||
orders?: PublicOrder[];
|
||||
onOrderClicked?: (id: number) => void;
|
||||
onOrderClicked?: (id: number, shortAlias: string) => void;
|
||||
zoom?: number;
|
||||
center?: [number, number];
|
||||
interactive?: boolean;
|
||||
@ -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 != null && onOrderClicked(order.id, order.coordinatorShortAlias);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{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 != null) || !(order?.longitude != null)) 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, type 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.getSlot()?.order?.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.getSlot()?.order?.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.getSlot()?.order;
|
||||
|
||||
if (order === undefined || 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) {
|
||||
handleStatusChange(oldOrderStatus, order.status);
|
||||
setOldOrderStatus(order.status);
|
||||
} else if (order != undefined && order.chat_last_index > oldChatIndex) {
|
||||
if (page != 'order') {
|
||||
notify(Messages.chatMessage);
|
||||
const order = garage.getSlot()?.order;
|
||||
if (order !== undefined && order !== null) {
|
||||
if (order.status !== oldOrderStatus) {
|
||||
handleStatusChange(oldOrderStatus, order.status);
|
||||
setOldOrderStatus(order.status);
|
||||
} else if (order.chat_last_index > oldChatIndex) {
|
||||
if (page !== 'order') {
|
||||
notify(Messages.chatMessage);
|
||||
}
|
||||
setOldChatIndex(order.chat_last_index);
|
||||
}
|
||||
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, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
|
||||
interface TakeButtonProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
baseUrl: string;
|
||||
info: Info;
|
||||
currentOrder: Order;
|
||||
info?: Info;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
@ -42,15 +42,15 @@ interface OpenDialogsProps {
|
||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
||||
|
||||
const TakeButton = ({
|
||||
order,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
currentOrder,
|
||||
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, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||
const [badRequest, setBadRequest] = useState<string>('');
|
||||
@ -58,15 +58,19 @@ const TakeButton = ({
|
||||
const [open, setOpen] = useState<OpenDialogsProps>(closeAll);
|
||||
const [satoshis, setSatoshis] = useState<string>('');
|
||||
|
||||
const satoshisNow = () => {
|
||||
const satoshisNow = (): string | undefined => {
|
||||
if (currentOrder === 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 amount = order.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
const btcNow = currentOrder.satoshis_now / 100000000;
|
||||
const rate =
|
||||
currentOrder.amount != null ? currentOrder.amount / btcNow : currentOrder.max_amount / btcNow;
|
||||
const amount =
|
||||
currentOrder.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
const satoshis = computeSats({
|
||||
amount,
|
||||
routingBudget: order.is_buyer ? defaultRoutingBudget : 0,
|
||||
routingBudget: currentOrder.is_buyer ? defaultRoutingBudget : 0,
|
||||
fee: tradeFee,
|
||||
rate,
|
||||
});
|
||||
@ -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 =
|
||||
currentOrder?.currency === 1000 ? 'Sats' : currencies[`${Number(currentOrder?.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,21 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
const amountHelperText = useMemo(() => {
|
||||
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
if (amount < Number(order.min_amount) && takeAmount != '') {
|
||||
if (currentOrder === null) return;
|
||||
|
||||
const amount =
|
||||
currentOrder.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
if (amount < Number(currentOrder.min_amount) && takeAmount !== '') {
|
||||
return t('Too low');
|
||||
} else if (amount > Number(order.max_amount) && takeAmount != '') {
|
||||
} else if (amount > Number(currentOrder.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 (currentOrder?.maker_status === 'Inactive') {
|
||||
setOpen({ inactiveMaker: true, confirmation: false });
|
||||
} else {
|
||||
setOpen({ inactiveMaker: false, confirmation: true });
|
||||
@ -160,17 +176,18 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
const invalidTakeAmount = useMemo(() => {
|
||||
const amount = order.currency == 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
const amount =
|
||||
currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount);
|
||||
return (
|
||||
amount < Number(order.min_amount) ||
|
||||
amount > Number(order.max_amount) ||
|
||||
takeAmount == '' ||
|
||||
amount < Number(currentOrder?.min_amount) ||
|
||||
amount > Number(currentOrder?.max_amount) ||
|
||||
takeAmount === '' ||
|
||||
takeAmount == null
|
||||
);
|
||||
}, [takeAmount, order]);
|
||||
}, [takeAmount, orderUpdatedAt]);
|
||||
|
||||
const takeOrderButton = function () {
|
||||
if (order.has_range) {
|
||||
const takeOrderButton = function (): JSX.Element {
|
||||
if (currentOrder?.has_range) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -209,8 +226,8 @@ const TakeButton = ({
|
||||
required={true}
|
||||
value={takeAmount}
|
||||
inputProps={{
|
||||
min: order.min_amount,
|
||||
max: order.max_amount,
|
||||
min: currentOrder?.min_amount,
|
||||
max: currentOrder?.max_amount,
|
||||
style: { textAlign: 'center' },
|
||||
}}
|
||||
onChange={handleTakeAmountChange}
|
||||
@ -260,10 +277,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
|
||||
{currentOrder?.type === 1
|
||||
? t('You will receive {{satoshis}} Sats (Approx)', { satoshis })
|
||||
: t('You will send {{satoshis}} Sats (Approx)', { satoshis })}
|
||||
</FormHelperText>
|
||||
@ -296,33 +313,46 @@ const TakeButton = ({
|
||||
}
|
||||
};
|
||||
|
||||
const takeOrder = function () {
|
||||
const takeOrder = function (): void {
|
||||
const robot = garage.getSlot()?.getRobot() ?? null;
|
||||
|
||||
if (currentOrder === null || robot === null) return;
|
||||
|
||||
setLoadingTake(true);
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(currentOrder.shortAlias)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
setCurrentOrderId({ id: null, shortAlias: null });
|
||||
apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/order/?order_id=' + order.id,
|
||||
url + basePath,
|
||||
`/api/order/?order_id=${String(currentOrder?.id)}`,
|
||||
{
|
||||
action: 'take',
|
||||
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
},
|
||||
{ tokenSHA256: robot.tokenSHA256 },
|
||||
{ tokenSHA256: robot?.tokenSHA256 },
|
||||
)
|
||||
.then((data) => {
|
||||
setLoadingTake(false);
|
||||
if (data.bad_request) {
|
||||
if (data?.bad_request !== undefined) {
|
||||
setBadRequest(data.bad_request);
|
||||
} else {
|
||||
setOrder(data);
|
||||
setCurrentOrderId({ id: currentOrder?.id, shortAlias: currentOrder?.shortAlias });
|
||||
setBadRequest('');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setBadRequest('Request error');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Countdown date={new Date(order.penalty)} renderer={countdownTakeOrderRenderer} />
|
||||
{badRequest != '' ? (
|
||||
<Countdown
|
||||
date={new Date(currentOrder?.penalty ?? '')}
|
||||
renderer={countdownTakeOrderRenderer}
|
||||
/>
|
||||
{badRequest !== '' ? (
|
||||
<Box style={{ padding: '0.5em' }}>
|
||||
<Typography align='center' color='secondary'>
|
||||
{t(badRequest)}
|
||||
@ -342,7 +372,7 @@ const TakeButton = ({
|
||||
setLoadingTake(true);
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
hasRobot={Boolean(garage.getSlot()?.hashId)}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
<InactiveMakerDialog />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useContext, useEffect } 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,57 +36,64 @@ import { PaymentStringAsIcons } from '../../components/PaymentMethods';
|
||||
import { FlagWithProps, SendReceiveIcon } from '../Icons';
|
||||
import LinearDeterminate from './LinearDeterminate';
|
||||
|
||||
import { type Order, type Info } from '../../models';
|
||||
import type Coordinator from '../../models';
|
||||
import { statusBadgeColor, pn, amountToString, computeSats } from '../../utils';
|
||||
import TakeButton from './TakeButton';
|
||||
import { F2fMapDialog } from '../Dialogs';
|
||||
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
import { type Order } from '../../models';
|
||||
|
||||
interface OrderDetailsProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
info: Info;
|
||||
baseUrl: string;
|
||||
hasRobot: boolean;
|
||||
shortAlias: string;
|
||||
currentOrder: Order;
|
||||
onClickCoordinator?: () => void;
|
||||
onClickGenerateRobot?: () => void;
|
||||
}
|
||||
|
||||
const OrderDetails = ({
|
||||
order,
|
||||
info,
|
||||
setOrder,
|
||||
baseUrl,
|
||||
hasRobot,
|
||||
shortAlias,
|
||||
currentOrder,
|
||||
onClickCoordinator = () => null,
|
||||
onClickGenerateRobot = () => null,
|
||||
}: OrderDetailsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const currencyCode: string = currencies[`${order.currency}`];
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const [coordinator, setCoordinator] = useState<Coordinator | null>(
|
||||
federation.getCoordinator(shortAlias),
|
||||
);
|
||||
const [currencyCode, setCurrencyCode] = useState<string | null>();
|
||||
const [showSatsDetails, setShowSatsDetails] = useState<boolean>(false);
|
||||
const [openWorldmap, setOpenWorldmap] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCoordinator(federation.getCoordinator(shortAlias));
|
||||
setCurrencyCode(currencies[(currentOrder?.currency ?? 1).toString()]);
|
||||
}, [currentOrder]);
|
||||
|
||||
const amountString = useMemo(() => {
|
||||
// precision to 8 decimal if currency is BTC otherwise 4 decimals
|
||||
if (order.currency == 1000) {
|
||||
if (currentOrder === null) return;
|
||||
|
||||
if (currentOrder.currency === 1000) {
|
||||
return (
|
||||
amountToString(
|
||||
order.amount * 100000000,
|
||||
order.amount ? false : order.has_range,
|
||||
order.min_amount * 100000000,
|
||||
order.max_amount * 100000000,
|
||||
(currentOrder.amount * 100000000).toString(),
|
||||
currentOrder.amount > 0 ? false : currentOrder.has_range,
|
||||
currentOrder.min_amount * 100000000,
|
||||
currentOrder.max_amount * 100000000,
|
||||
) + ' Sats'
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
amountToString(
|
||||
order.amount,
|
||||
order.amount ? false : order.has_range,
|
||||
order.min_amount,
|
||||
order.max_amount,
|
||||
) + ` ${currencyCode}`
|
||||
currentOrder.amount?.toString(),
|
||||
currentOrder.amount > 0 ? false : currentOrder.has_range,
|
||||
currentOrder.min_amount,
|
||||
currentOrder.max_amount,
|
||||
) + ` ${String(currencyCode)}`
|
||||
);
|
||||
}
|
||||
}, [order.currency, order.amount, order.min_amount, order.max_amount, order.has_range]);
|
||||
}, [currentOrder, currencyCode]);
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
const countdownRenderer = function ({
|
||||
@ -95,13 +102,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?.total_secs_exp ?? 1);
|
||||
// Make orange at 25% of time left
|
||||
if (fraction_left < 0.25) {
|
||||
color = theme.palette.warning.main;
|
||||
@ -121,18 +128,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 +168,20 @@ const OrderDetails = ({
|
||||
let send: string = '';
|
||||
let receive: string = '';
|
||||
let sats: string = '';
|
||||
const order = currentOrder;
|
||||
|
||||
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,18 +201,17 @@ const OrderDetails = ({
|
||||
routingBudget: defaultRoutingBudget,
|
||||
rate,
|
||||
});
|
||||
sats = `${min}-${max}`;
|
||||
sats = `${String(min)}-${String(max)}`;
|
||||
}
|
||||
send = t('You send via {{method}} {{amount}}', {
|
||||
amount: amountString,
|
||||
method: order.payment_method,
|
||||
currencyCode,
|
||||
});
|
||||
receive = t('You receive via Lightning {{amount}} Sats (Approx)', {
|
||||
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}}', {
|
||||
@ -217,64 +236,85 @@ const OrderDetails = ({
|
||||
method: order.payment_method,
|
||||
});
|
||||
}
|
||||
|
||||
return { send, receive };
|
||||
}, [order.currency, order.satoshis_now, order.amount, order.has_range]);
|
||||
}, [currentOrder, amountString]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={0}>
|
||||
<F2fMapDialog
|
||||
latitude={order.latitude}
|
||||
longitude={order.longitude}
|
||||
latitude={currentOrder?.latitude}
|
||||
longitude={currentOrder?.longitude}
|
||||
open={openWorldmap}
|
||||
orderType={order.type || 0}
|
||||
orderType={currentOrder?.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 shortAlias={coordinator.shortAlias} small={true} smooth={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}
|
||||
baseUrl={baseUrl}
|
||||
statusColor={statusBadgeColor(currentOrder?.maker_status ?? '')}
|
||||
hashId={currentOrder?.maker_hash_id}
|
||||
tooltip={t(currentOrder?.maker_status ?? '')}
|
||||
orderType={currentOrder?.type}
|
||||
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={`${String(currentOrder?.maker_nick)} (${
|
||||
currentOrder?.type === 1
|
||||
? t(currentOrder?.currency === 1000 ? 'Swapping Out' : 'Seller')
|
||||
: t(currentOrder?.currency === 1000 ? 'Swapping In' : 'Buyer')
|
||||
})`}
|
||||
secondary={t('Order maker')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={order.is_participant && order.taker_nick !== 'None'}>
|
||||
<Collapse in={currentOrder?.is_participant && currentOrder?.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={`${String(currentOrder?.taker_nick)} (${
|
||||
currentOrder?.type === 1
|
||||
? t(currentOrder?.currency === 1000 ? 'Swapping In' : 'Buyer')
|
||||
: t(currentOrder?.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}
|
||||
baseUrl={baseUrl}
|
||||
statusColor={statusBadgeColor(currentOrder?.taker_status ?? '')}
|
||||
hashId={
|
||||
currentOrder?.taker_hash_id === 'None' ? undefined : currentOrder?.taker_hash_id
|
||||
}
|
||||
tooltip={t(currentOrder?.taker_status ?? '')}
|
||||
orderType={currentOrder?.type === 0 ? 1 : 0}
|
||||
small={true}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
@ -284,12 +324,15 @@ const OrderDetails = ({
|
||||
<Chip label={t('Order Details')} />
|
||||
</Divider>
|
||||
|
||||
<Collapse in={order.is_participant}>
|
||||
<Collapse in={currentOrder?.is_participant}>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Article />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(order.status_message)} secondary={t('Order status')} />
|
||||
<ListItemText
|
||||
primary={t(currentOrder?.status_message ?? '')}
|
||||
secondary={t('Order status')}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Collapse>
|
||||
@ -311,7 +354,7 @@ const OrderDetails = ({
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={amountString}
|
||||
secondary={order.amount ? 'Amount' : 'Amount Range'}
|
||||
secondary={(currentOrder?.amount ?? 0) > 0 ? 'Amount' : 'Amount Range'}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<IconButton
|
||||
@ -360,18 +403,24 @@ const OrderDetails = ({
|
||||
size={1.42 * theme.typography.fontSize}
|
||||
othersText={t('Others')}
|
||||
verbose={true}
|
||||
text={order.payment_method}
|
||||
text={currentOrder?.payment_method}
|
||||
/>
|
||||
}
|
||||
secondary={
|
||||
order.currency == 1000 ? t('Swap destination') : t('Accepted payment methods')
|
||||
currentOrder?.currency === 1000
|
||||
? t('Swap destination')
|
||||
: t('Accepted payment methods')
|
||||
}
|
||||
/>
|
||||
{order.payment_method.includes('Cash F2F') && (
|
||||
{currentOrder?.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 +436,27 @@ const OrderDetails = ({
|
||||
<PriceChange />
|
||||
</ListItemIcon>
|
||||
|
||||
{order.price_now !== undefined ? (
|
||||
{currentOrder?.price_now !== undefined ? (
|
||||
<ListItemText
|
||||
primary={t('{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%', {
|
||||
price: pn(order.price_now),
|
||||
price: pn(currentOrder?.price_now),
|
||||
currencyCode,
|
||||
premium: order.premium_now,
|
||||
premium: currentOrder?.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?.price_now === undefined && currentOrder?.is_explicit ? (
|
||||
<ListItemText
|
||||
primary={pn(currentOrder?.satoshis)}
|
||||
secondary={t('Amount of Satoshis')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!order.price_now && !order.is_explicit ? (
|
||||
{currentOrder?.price_now === undefined && !currentOrder?.is_explicit ? (
|
||||
<ListItemText
|
||||
primary={parseFloat(Number(order.premium).toFixed(2)) + '%'}
|
||||
primary={`${parseFloat(Number(currentOrder?.premium).toFixed(2))}%`}
|
||||
secondary={t('Premium over market price')}
|
||||
/>
|
||||
) : null}
|
||||
@ -418,7 +470,7 @@ const OrderDetails = ({
|
||||
</ListItemIcon>
|
||||
<Grid container>
|
||||
<Grid item xs={4.5}>
|
||||
<ListItemText primary={order.id} secondary={t('Order ID')} />
|
||||
<ListItemText primary={currentOrder?.id} secondary={t('Order ID')} />
|
||||
</Grid>
|
||||
<Grid item xs={7.5}>
|
||||
<Grid container>
|
||||
@ -429,7 +481,7 @@ const OrderDetails = ({
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<ListItemText
|
||||
primary={timerRenderer(order.escrow_duration)}
|
||||
primary={timerRenderer(currentOrder?.escrow_duration)}
|
||||
secondary={t('Deposit timer')}
|
||||
></ListItemText>
|
||||
</Grid>
|
||||
@ -439,39 +491,45 @@ 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?.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?.expires_at ?? '')}
|
||||
renderer={countdownRenderer}
|
||||
/>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<LinearDeterminate totalSecsExp={order.total_secs_exp} expiresAt={order.expires_at} />
|
||||
<LinearDeterminate
|
||||
totalSecsExp={currentOrder?.total_secs_exp ?? 0}
|
||||
expiresAt={currentOrder?.expires_at ?? ''}
|
||||
/>
|
||||
</Collapse>
|
||||
</List>
|
||||
|
||||
{/* If the user has a penalty/limit */}
|
||||
{order.penalty !== undefined ? (
|
||||
{currentOrder?.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?.penalty ?? '')}
|
||||
renderer={countdownPenaltyRenderer}
|
||||
/>
|
||||
</Alert>
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!order.is_participant ? (
|
||||
{!currentOrder?.is_participant ? (
|
||||
<Grid item xs={12}>
|
||||
<TakeButton
|
||||
order={order}
|
||||
setOrder={setOrder}
|
||||
baseUrl={baseUrl}
|
||||
hasRobot={hasRobot}
|
||||
info={info}
|
||||
currentOrder={currentOrder}
|
||||
info={coordinator.info}
|
||||
onClickGenerateRobot={onClickGenerateRobot}
|
||||
/>
|
||||
</Grid>
|
||||
|
@ -328,11 +328,6 @@ const icons = {
|
||||
image:
|
||||
'data:image/webp;base64,UklGRgQIAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSF0CAAABoC3JtmnbasvHtm3btm3btm3btm3btm37nHvn7A9zTI3RPyAiJgCu+6KnyVW8SI7kET1Q35+984or30isvz85u0lyj0KB8kvfkX393vgcHjXijXhFjl9tE06+hLP/kKtve4eXK9ywn+T6iwYeiYo/JCkPJJMlNFEjSb82kiPeKZJ4blCCLM9J6iPRXCv0hSS/Hs+lwj9I+tvxXMnxhRS8VrT38oOnjm4YVy+RvSSvSW392oCE1sJfIfX/W5PRyhJi8f+5UU3qksL6/5qm6QKiZaI4H1US6pqmG2aKVhOHuk5E/QWFdR6IiGoZPOeIz7SGKsTnZ6/hPCPbAaAQMdrOsIaR/+MDiP6Hkb0A0IoYrWzYx8hNL4CIfxmpAgAViM+dMI7j4218wTE2/hSB0fuViz+VIUxMXD6EuDgb/0KipmxQElFfPvKIJvJRTrSUj1qijXzU52oxHzVFE/goK+rDR25RYz4Si4qy8TcoSsDGPYg9n7nYbILDXAwyG81FKbMyTPyNaBb+Nw9HYHEHDz2sNGVBS2ol8k8OTsDyMg6aWsvDwPtw1nBMveGwWUa5bzHteI6pNgy2c2tqvYpkDwvVagAHo79WaY/HCVTW1fmUCM7OUEavAYdDp1WZCMfjPlFjp985pP+owtmIcDPnJ/kuR4e72d/JdjY63E51X67tEeB+9L0S6WN9kNE36J8s7ytD1lxX5dgYF/IGu392724FyB19zFd3HrcOQvqoPe45ph2rE4CS3vxTH+r2/j8/ICUU9qRoMuPIy/9Evx/sGl01JjgMxc2UO3eaGH7ICgBWUDgggAUAAFAZAJ0BKlAAUAA+bTKSRyQjIaEnuA1wgA2JbADIU986t9R+IHJUbbd4t6FOp1i+R/SN6gPMA/TfpE+YD9repR6AH9b/4XWAfs57AHlqfuj8I39r/7XpMZqJ2WSLFa5mXqsZqXk2fRNP/OPdDWR/Ni0kTCfSkFL6tovPlcrsJVeTsSQpTWiexkWX7MZsN6w9K2ME0rSE39D/wMtLRlF44ZadDMmwB9o11wXDkOllLTckufUN9WkQRM1KyNzZLvp5Ju+b2/wjUIdZvpTV/ZHyPRTxVwAA/rT///R+v6f/nDb0jLf/vtTl8mz8qlFYj5SByGk9H4YNSypkJK1ekucsY9WBLpKoOMRzEhnQzjhRAQQX1BcJ3YxQybzzpl074CzcViS5AL8OgdyXSYPa7Emy7zDpXWrVWM//T024rOSZz6XZumZmAt51u1PgXDoBK1MUkT+GynCVaeAFmvv/6cbqaeCAGh+tl4P/z1W1JPVY1C178f///9JtCg/9vwILIGL0fpRTfrp4dVQGhnHTnveZ0Ka2PILfgai1KAHILUQXjtRQ7MI7qFwSz3CztIbXr50kEPTI2c47sjsPpLVrLGeqsbpryhUd9nmK8IAf87lhKQpYC5bJqmCEYIkLJSu6Bt/EXRE2XWgyWfsYLqG/9yPSOFU5rfuYWwA5SE+FbiRg8xDpyhdB9lW9j3qoPGIRcOMx1xdQNxWGRzGDVCPVvO5a2N47svRjJHR/85QMMj/knP/kZK/iabEp1vpHEqzJu5MP/PAeISbSY0+L4m6+FxphLcC4ggIPJNN0uc4l4eMD6Wba1KB7g04URnGgSaMFzRYgb6vDCtWfTJlfl4K+65I7j809jkSFGqyz9wDqqDjcy4z9Wv4fJvPmuN71dusz7f1tukP2eBJKY3HBFSd1Ff4b4R0i6WyfE/d4zwSDwRBNUXQ/D7XYRLfJdhCtY/zlIzUL/sgBbCD64fDc0ipTxRL+AK5VgSefVXCAojZV32NLZjHhlaDXfkrd8of4HZ58imxXx8/rVZz+S+a+vSwu9Rg1Hv6pHipcgQarnmGTRdZUh8PW7ciLHeU6NX/nIFY/kZsTP/GfcOPegdwMxJzCKmsfjTJMYHgC8ghqFQEzahIPW2hk+cxWRz66prI7qnnNICufWurRhoZXLADIKgxgJPTTxK4PX8bjYRui16ft1OjYeD8oIxBcvIDmrC4sh9icub6cjzJbZQL7zD0Mjn8noSEEG8yXWcDfynSmnvif+QTzgpcmNOGu0GoesK5tbTOu0hAFKAdbtjsZ/jBVEeLncPbUclvsRxOJT9ijJWVSWAEqu1Vx+9o1ulklUjVMnjwFzcEQnX3hDpOC0/reKh3RABaoJMtT91PKguKfSc//YEgXwz9exLyhBGSy36toILX0HHpU8+1Km7RgMQq60jls6nyLlrTvfnPpTXMFwOYciQvHCsrsslv6dnXfyk6QiLdBmx+tbYdeLvXroBLnE5vAYCORNN+97K8qv8p3Er1+MW569KSTOJjQO72lLuMezUriWLcb8vb505n0ZBkXDMBNPlq38O5H1pyZdspPSMAiLUnvXOlsKEwyaCX6ybdPNzTgTqi5tVApJ/Cs3Rc9ZW15skGPSERtlGIZRgbpywK0lHbhw0kF/+pBg+KnmS9XX5QvOM+9rFWoObbSn/dYDzqi8J3yRJrO7y1HjyPm26TukHPJ+hlkfdbPwmKfrQV/KqdU5PxqPg17o7VJv0/zg176cF56iJyuQFXHKeNbgyDYFuSNyOGifmbDHnH7AUppac3xt7NGGgecUybFxmvY6I5qASx3sjARQtPCzgbxOawronXRaByjZ4kepHDwAu4bt/sEK3/3Af//0br//Rnv//6LsAAAAAA=',
|
||||
},
|
||||
rebellion: {
|
||||
title: 'rebellion',
|
||||
image:
|
||||
'data:image/webp;base64,UklGRqIAAABXRUJQVlA4IJYAAACQBgCdASpQAFAAPw12s1EsJySir1qoAYAhiUAawngAUY6/lgEE0qi55dB7tvExo7HfHc16q0htjYtRhb8AAP7t7Wf/5gn/v9/3+83udj26AfxRS7bxpqkjTEeT30z7aAexBz6OMlS5/Mu8+4L9Z/Q7a1vRN3t5NQxDbUAIw96JhAAAw+jRL7n/5ZammZlHFXX1moAAAAA=',
|
||||
},
|
||||
revolut: {
|
||||
title: 'revolut',
|
||||
image:
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 944 B |
@ -9,7 +9,6 @@ export const fiatMethods: PaymentMethod[] = [
|
||||
{ name: 'Zelle', icon: 'zelle' },
|
||||
{ name: 'Strike', icon: 'strike' },
|
||||
{ name: 'WeChat Pay', icon: 'wechatpay' },
|
||||
{ name: 'Rebellion', icon: 'rebellion' },
|
||||
{ name: 'Instant SEPA', icon: 'sepa' },
|
||||
{ name: 'Interac e-Transfer', icon: 'interac' },
|
||||
{ name: 'Wise', icon: 'wise' },
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
113
frontend/src/components/RobotAvatar/RobohashGenerator.ts
Normal file
113
frontend/src/components/RobotAvatar/RobohashGenerator.ts
Normal file
@ -0,0 +1,113 @@
|
||||
interface Task {
|
||||
robohash: Robohash;
|
||||
resolves: Array<(result: string) => void>;
|
||||
rejects: Array<(reason?: Error) => void>;
|
||||
}
|
||||
|
||||
interface Robohash {
|
||||
hash: string;
|
||||
size: 'small' | 'large';
|
||||
cacheKey: string;
|
||||
}
|
||||
|
||||
interface RoboWorker {
|
||||
worker: Worker;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
class RoboGenerator {
|
||||
private assetsCache: Record<string, string> = {};
|
||||
|
||||
private readonly workers: RoboWorker[] = [];
|
||||
private readonly queue: Task[] = [];
|
||||
|
||||
constructor() {
|
||||
// limit to 8 workers
|
||||
const numCores = 8;
|
||||
|
||||
for (let i = 0; i < numCores; i++) {
|
||||
const worker = new Worker(new URL('./robohash.worker.ts', import.meta.url));
|
||||
worker.onmessage = this.assignTasksToWorkers.bind(this);
|
||||
this.workers.push({ worker, busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
private assignTasksToWorkers(): void {
|
||||
const availableWorker = this.workers.find((w) => !w.busy);
|
||||
|
||||
if (availableWorker) {
|
||||
const task = this.queue.shift();
|
||||
if (task) {
|
||||
availableWorker.busy = true;
|
||||
availableWorker.worker.postMessage(task.robohash);
|
||||
|
||||
// Clean up the event listener and free the worker after receiving the result
|
||||
const cleanup = (): void => {
|
||||
availableWorker.worker.removeEventListener('message', completionCallback);
|
||||
availableWorker.busy = false;
|
||||
};
|
||||
|
||||
// Resolve the promise when the task is completed
|
||||
const completionCallback = (event: MessageEvent): void => {
|
||||
if (event.data.cacheKey === task.robohash.cacheKey) {
|
||||
const { cacheKey, imageUrl } = event.data;
|
||||
|
||||
// Update the cache and resolve the promise
|
||||
this.assetsCache[cacheKey] = imageUrl;
|
||||
|
||||
cleanup();
|
||||
|
||||
task.resolves.forEach((f) => {
|
||||
f(imageUrl);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
availableWorker.worker.addEventListener('message', completionCallback);
|
||||
|
||||
// Reject the promise if an error occurs
|
||||
availableWorker.worker.addEventListener('error', (error) => {
|
||||
cleanup();
|
||||
|
||||
task.rejects.forEach((f) => {
|
||||
f(new Error(error.message));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public generate: (hash: string, size: 'small' | 'large') => Promise<string> = async (
|
||||
hash,
|
||||
size,
|
||||
) => {
|
||||
const cacheKey = `${size}px;${hash}`;
|
||||
if (this.assetsCache[cacheKey]) {
|
||||
return this.assetsCache[cacheKey];
|
||||
} else {
|
||||
return await new Promise((resolve, reject) => {
|
||||
let task = this.queue.find((t) => t.robohash.cacheKey === cacheKey);
|
||||
|
||||
if (!task) {
|
||||
task = {
|
||||
robohash: {
|
||||
hash,
|
||||
size,
|
||||
cacheKey,
|
||||
},
|
||||
resolves: [],
|
||||
rejects: [],
|
||||
};
|
||||
this.queue.push(task);
|
||||
}
|
||||
|
||||
task.resolves.push(resolve);
|
||||
task.rejects.push(reject);
|
||||
|
||||
this.assignTasksToWorkers();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const robohash = new RoboGenerator();
|
@ -1,12 +1,14 @@
|
||||
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 { robohash } from './RobohashGenerator';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
|
||||
interface Props {
|
||||
nickname: string | undefined;
|
||||
shortAlias?: string | undefined;
|
||||
hashId?: string | undefined;
|
||||
smooth?: boolean;
|
||||
small?: boolean;
|
||||
flipHorizontally?: boolean;
|
||||
@ -19,7 +21,6 @@ interface Props {
|
||||
tooltipPosition?: string;
|
||||
avatarClass?: string;
|
||||
onLoad?: () => void;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
interface BackgroundData {
|
||||
@ -28,7 +29,8 @@ interface BackgroundData {
|
||||
}
|
||||
|
||||
const RobotAvatar: React.FC<Props> = ({
|
||||
nickname,
|
||||
shortAlias,
|
||||
hashId,
|
||||
orderType,
|
||||
statusColor,
|
||||
tooltip,
|
||||
@ -41,34 +43,53 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
avatarClass = 'flippedSmallAvatar',
|
||||
imageStyle = {},
|
||||
onLoad = () => {},
|
||||
baseUrl,
|
||||
}) => {
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>();
|
||||
const [nicknameReady, setNicknameReady] = useState<boolean>(false);
|
||||
const [avatarSrc, setAvatarSrc] = useState<string>('');
|
||||
const [activeBackground, setActiveBackground] = useState<boolean>(true);
|
||||
const { hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const backgroundFadeTime = 3000;
|
||||
|
||||
const [backgroundData] = useState<BackgroundData>(
|
||||
placeholderType == 'generating' ? placeholder.generating : placeholder.loading,
|
||||
);
|
||||
const [backgroundData] = useState<BackgroundData>(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) {
|
||||
// TODO: HANDLE ANDROID AVATARS TOO (when window.NativeRobosats !== undefined)
|
||||
if (hashId !== undefined) {
|
||||
robohash
|
||||
.generate(hashId, small ? 'small' : 'large')
|
||||
.then((avatar) => {
|
||||
setAvatarSrc(avatar);
|
||||
})
|
||||
.catch(() => {
|
||||
setAvatarSrc('');
|
||||
});
|
||||
setTimeout(() => {
|
||||
setActiveBackground(false);
|
||||
}, backgroundFadeTime);
|
||||
}
|
||||
}, [hashId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shortAlias !== undefined) {
|
||||
if (window.NativeRobosats === undefined) {
|
||||
setAvatarSrc(`${baseUrl}/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`);
|
||||
setNicknameReady(true);
|
||||
setAvatarSrc(
|
||||
`${hostUrl}/static/federation/avatars/${shortAlias}${small ? '.small' : ''}.webp`,
|
||||
);
|
||||
} else {
|
||||
setNicknameReady(true);
|
||||
apiClient
|
||||
.fileImageUrl(baseUrl, `/static/assets/avatars/${nickname}${small ? '.small' : ''}.webp`)
|
||||
.then(setAvatarSrc);
|
||||
setAvatarSrc(
|
||||
`file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}${
|
||||
small ? ' .small' : ''
|
||||
}.webp`,
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setActiveBackground(false);
|
||||
}, backgroundFadeTime);
|
||||
} else {
|
||||
setNicknameReady(false);
|
||||
setActiveBackground(true);
|
||||
}
|
||||
}, [nickname]);
|
||||
}, [shortAlias]);
|
||||
|
||||
const statusBadge = (
|
||||
<div style={{ position: 'relative', left: '0.428em', top: '0.07em' }}>
|
||||
@ -99,15 +120,12 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
>
|
||||
<div className={className}>
|
||||
<SmoothImage
|
||||
src={nicknameReady ? avatarSrc : null}
|
||||
src={avatarSrc}
|
||||
imageStyles={{
|
||||
borderRadius: '50%',
|
||||
border: '0.3px solid #55555',
|
||||
filter: 'dropShadow(0.5px 0.5px 0.5px #000000)',
|
||||
...imageStyle,
|
||||
onLoad: setTimeout(() => {
|
||||
setActiveBackground(false);
|
||||
}, 5000),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -118,8 +136,8 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
<Avatar
|
||||
className={avatarClass}
|
||||
style={style}
|
||||
alt={nickname}
|
||||
src={nicknameReady ? avatarSrc : null}
|
||||
alt={hashId ?? shortAlias ?? 'unknown'}
|
||||
src={avatarSrc}
|
||||
imgProps={{
|
||||
sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
|
||||
style: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
|
||||
@ -128,12 +146,12 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [nickname, nicknameReady, avatarSrc, statusColor, tooltip, avatarClass]);
|
||||
}, [hashId, shortAlias, avatarSrc, statusColor, tooltip, avatarClass, activeBackground]);
|
||||
|
||||
const getAvatarWithBadges = useCallback(() => {
|
||||
let component = avatar;
|
||||
|
||||
if (statusColor) {
|
||||
if (statusColor !== undefined) {
|
||||
component = (
|
||||
<Badge variant='dot' overlap='circular' badgeContent='' color={statusColor}>
|
||||
{component}
|
||||
@ -153,7 +171,7 @@ const RobotAvatar: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
if (tooltip !== undefined) {
|
||||
component = (
|
||||
<Tooltip placement={tooltipPosition} enterTouchDelay={0} title={tooltip}>
|
||||
{component}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
17
frontend/src/components/RobotAvatar/robohash.worker.ts
Normal file
17
frontend/src/components/RobotAvatar/robohash.worker.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { async_generate_robohash } from 'robo-identities-wasm';
|
||||
|
||||
// Listen for messages from the main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
void (async () => {
|
||||
const { hash, size, cacheKey } = event.data;
|
||||
|
||||
// Generate the image using async_image_base
|
||||
const t0 = performance.now();
|
||||
const avatarB64: string = await async_generate_robohash(hash, size === 'small' ? 80 : 256);
|
||||
const imageUrl = `data:image/png;base64,${avatarB64}`;
|
||||
const t1 = performance.now();
|
||||
console.log(`Avatar generated in: ${t1 - t0} ms`);
|
||||
// Send the result back to the main thread
|
||||
self.postMessage({ cacheKey, imageUrl });
|
||||
})();
|
||||
});
|
369
frontend/src/components/RobotInfo/index.tsx
Normal file
369
frontend/src/components/RobotInfo/index.tsx
Normal file
@ -0,0 +1,369 @@
|
||||
import React, { useContext, useEffect, 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 } 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';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface Props {
|
||||
coordinator: Coordinator;
|
||||
onClose: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) => {
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
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(() => {
|
||||
void handleWebln();
|
||||
}, []);
|
||||
|
||||
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
e.preventDefault();
|
||||
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
|
||||
if (robot != null && 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 slot = garage.getSlot();
|
||||
const robot = slot?.getRobot(coordinator.shortAlias);
|
||||
|
||||
if (robot != null && slot?.token != null && robot.encPrivKey != null) {
|
||||
void signCleartextMessage(rewardInvoice, robot.encPrivKey, slot?.token).then(
|
||||
(signedInvoice) => {
|
||||
console.log('Signed message:', signedInvoice);
|
||||
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
|
||||
console.log(data);
|
||||
setBadInvoice(data.bad_invoice ?? '');
|
||||
setShowRewardsSpinner(false);
|
||||
setWithdrawn(data.successful_withdrawal);
|
||||
setOpenClaimRewards(!data.successful_withdrawal);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const setStealthInvoice = (wantsStealth: boolean): void => {
|
||||
const slot = garage.getSlot();
|
||||
if (slot?.token != null) {
|
||||
void coordinator.fetchStealth(wantsStealth, garage, slot?.token);
|
||||
}
|
||||
};
|
||||
|
||||
const slot = garage.getSlot();
|
||||
const robot = slot?.getRobot(coordinator.shortAlias);
|
||||
|
||||
return (
|
||||
<Accordion disabled={disabled}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
{`${coordinator.longAlias}:`}
|
||||
{(robot?.earnedRewards ?? 0) > 0 && (
|
||||
<Typography color='success'> {t('Claim Sats!')} </Typography>
|
||||
)}
|
||||
{slot?.activeShortAlias === coordinator.shortAlias && (
|
||||
<Typography color='success'>
|
||||
<b>{t('Active order!')}</b>
|
||||
</Typography>
|
||||
)}
|
||||
{slot?.lastShortAlias === coordinator.shortAlias && (
|
||||
<Typography color='warning'> {t('finished order')}</Typography>
|
||||
)}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List dense disablePadding={true}>
|
||||
{slot?.activeShortAlias === coordinator.shortAlias ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setCurrentOrderId({
|
||||
id: slot?.activeShortAlias,
|
||||
shortAlias: slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
|
||||
});
|
||||
navigate(
|
||||
`/order/${String(slot?.activeShortAlias)}/${String(
|
||||
slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
|
||||
)}`,
|
||||
);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent='' color='primary'>
|
||||
<Numbers color='primary' />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('One active order #{{orderID}}', {
|
||||
orderID: slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
|
||||
})}
|
||||
secondary={t('Your current order')}
|
||||
/>
|
||||
</ListItemButton>
|
||||
) : (robot?.lastOrderId ?? 0) > 0 && slot?.lastShortAlias === coordinator.shortAlias ? (
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
setCurrentOrderId({
|
||||
id: slot?.activeShortAlias,
|
||||
shortAlias: slot?.getRobot(slot?.activeShortAlias ?? '')?.lastOrderId,
|
||||
});
|
||||
navigate(
|
||||
`/order/${String(slot?.lastShortAlias)}/${String(
|
||||
slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
|
||||
)}`,
|
||||
);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Numbers color='primary' />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t('Your last order #{{orderID}}', {
|
||||
orderID: slot?.getRobot(slot?.lastShortAlias ?? '')?.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>{`${String(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 }}
|
||||
disabled={rewardInvoice === ''}
|
||||
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 (
|
||||
|
@ -29,14 +29,16 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { systemClient } from '../../services/System';
|
||||
import SwapCalls from '@mui/icons-material/SwapCalls';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
|
||||
interface SettingsFormProps {
|
||||
dense?: boolean;
|
||||
showNetwork?: boolean;
|
||||
}
|
||||
|
||||
const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps): JSX.Element => {
|
||||
const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext);
|
||||
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
||||
const { fav, setFav, origin, hostUrl, settings, setSettings } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const fontSizes = [
|
||||
@ -176,8 +178,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,30 +217,27 @@ const SettingsForm = ({ dense = false, showNetwork = false }: SettingsFormProps)
|
||||
</ToggleButtonGroup>
|
||||
</ListItem>
|
||||
|
||||
{showNetwork ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Link />
|
||||
</ListItemIcon>
|
||||
<ToggleButtonGroup
|
||||
exclusive={true}
|
||||
value={settings.network}
|
||||
onChange={(e, network) => {
|
||||
setSettings({ ...settings, network });
|
||||
systemClient.setItem('settings_network', network);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value='mainnet' color='primary'>
|
||||
{t('Mainnet')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value='testnet' color='secondary'>
|
||||
{t('Testnet')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Link />
|
||||
</ListItemIcon>
|
||||
<ToggleButtonGroup
|
||||
exclusive={true}
|
||||
value={settings.network}
|
||||
onChange={(e, network) => {
|
||||
setSettings({ ...settings, network });
|
||||
void federation.updateUrls(origin, { ...settings, network }, hostUrl);
|
||||
systemClient.setItem('settings_network', network);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value='mainnet' color='primary'>
|
||||
{t('Mainnet')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value='testnet' color='secondary'>
|
||||
{t('Testnet')}
|
||||
</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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user