mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-20 13:34:30 +01:00
Feat/nextjs (#25)
* feat: initial nextjs commit * chore: general card styles changes * chore: add storybook * chore: small changes and fixes * fix: trading filter encoding * fix: add link to node * chore: set to correct version
This commit is contained in:
parent
d0f6a038a9
commit
aa60d618f9
577 changed files with 21212 additions and 28048 deletions
11
.babelrc
Normal file
11
.babelrc
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [
|
||||
"emotion",
|
||||
"inline-react-svg",
|
||||
[
|
||||
"styled-components",
|
||||
{ "ssr": true, "displayName": true, "preprocess": false }
|
||||
]
|
||||
]
|
||||
}
|
3
.commitlintrc.json
Normal file
3
.commitlintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
@ -0,0 +1,12 @@
|
|||
.git
|
||||
.gitignore
|
||||
.cache
|
||||
*.md
|
||||
!README*.md
|
||||
/node_modules
|
||||
/.next
|
||||
/docs
|
||||
/.github
|
||||
.env
|
||||
.vscode
|
||||
CHANGELOG.md
|
32
.gitignore
vendored
32
.gitignore
vendored
|
@ -1 +1,31 @@
|
|||
node_modules
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
/.next
|
||||
/.node_modules
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"arrowParens": "avoid"
|
||||
}
|
24
.storybook/config.js
Normal file
24
.storybook/config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { configure, addDecorator, addParameters } from '@storybook/react';
|
||||
import themeDecorator from './themeDecorator';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||
|
||||
const customViewports = {
|
||||
smallScreen: {
|
||||
name: 'Small Screen',
|
||||
styles: {
|
||||
width: '578px',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
addParameters({
|
||||
viewport: {
|
||||
viewports: { ...INITIAL_VIEWPORTS, ...customViewports },
|
||||
},
|
||||
});
|
||||
addDecorator(themeDecorator);
|
||||
addDecorator(withKnobs);
|
||||
|
||||
configure(require.context('../src/', true, /\.stories\.tsx?$/), module);
|
7
.storybook/main.js
Normal file
7
.storybook/main.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
addons: [
|
||||
'@storybook/addon-knobs/register',
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-viewport/register',
|
||||
],
|
||||
};
|
|
@ -2,6 +2,6 @@ import { addons } from '@storybook/addons';
|
|||
import { themes } from '@storybook/theming';
|
||||
|
||||
addons.setConfig({
|
||||
theme: themes.dark,
|
||||
panelPosition: 'right',
|
||||
theme: themes.dark,
|
||||
panelPosition: 'right',
|
||||
});
|
35
.storybook/themeDecorator.js
Normal file
35
.storybook/themeDecorator.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import styled, { ThemeProvider, css } from 'styled-components';
|
||||
import { select, boolean } from '@storybook/addon-knobs';
|
||||
import { backgroundColor, cardColor } from '../src/styles/Themes';
|
||||
|
||||
const StyledBackground = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 100px 0;
|
||||
${({ withBackground, cardBackground }) =>
|
||||
withBackground &&
|
||||
css`
|
||||
background: ${cardBackground ? cardColor : backgroundColor};
|
||||
`}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ThemeDecorator = storyFn => {
|
||||
const background = boolean('No Background', false);
|
||||
const cardBackground = boolean('Card Background', true);
|
||||
return (
|
||||
<ThemeProvider theme={{ mode: select('Theme', ['dark', 'light'], 'dark') }}>
|
||||
<StyledBackground
|
||||
withBackground={!background}
|
||||
cardBackground={cardBackground}
|
||||
>
|
||||
{storyFn()}
|
||||
</StyledBackground>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeDecorator;
|
12
.storybook/webpack.config.js
Normal file
12
.storybook/webpack.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = ({ config }) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [require.resolve('babel-preset-react-app')],
|
||||
},
|
||||
});
|
||||
|
||||
config.resolve.extensions.push('.ts', '.tsx');
|
||||
return config;
|
||||
};
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
5
@types/index.d.ts
vendored
Normal file
5
@types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.svg';
|
||||
declare module '*.gif';
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
FROM node:alpine
|
||||
|
||||
# Create app directory
|
||||
# RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /usr/src/app/
|
||||
COPY yarn.lock /usr/src/app/
|
||||
RUN yarn install --production=true
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
RUN yarn build
|
||||
EXPOSE 3000
|
||||
CMD [ "yarn", "start" ]
|
52
README.md
52
README.md
|
@ -1,7 +1,7 @@
|
|||
# **ThunderHub - Lightning Node Manager**
|
||||
|
||||

|
||||
[](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [](https://snyk.io/test/github/apotdevin/thunderhub) [](https://snyk.io/test/github/apotdevin/thunderhub) [](https://lerna.js.org/)
|
||||

|
||||
[](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE)
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
|
@ -16,21 +16,13 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
|||
|
||||
### Tech Stack
|
||||
|
||||
The repository consists of two packages (client and server) and is maintained with LernaJS and Yarn Workspaces.
|
||||
|
||||
#### Client
|
||||
|
||||
[](https://snyk.io/test/github/apotdevin/thunderhub)
|
||||
This repository consists of a **NextJS** server that handles both the backend **Graphql Server** and the frontend **React App**.
|
||||
|
||||
- NextJS
|
||||
- ReactJS
|
||||
- Typescript
|
||||
- Styled-Components
|
||||
- Apollo
|
||||
|
||||
#### Server
|
||||
|
||||
[](https://snyk.io/test/github/apotdevin/thunderhub)
|
||||
|
||||
- Apollo-Server
|
||||
- GraphQL
|
||||
- Ln-Service
|
||||
|
@ -99,47 +91,29 @@ git clone https://github.com/apotdevin/thunderhub.git
|
|||
- Node installed
|
||||
- Yarn installed
|
||||
|
||||
After cloning the repository run `yarn` to get all the necessary modules installed. Yarn workspaces will handle installing modules for both the client and the server.
|
||||
After cloning the repository run `yarn` to get all the necessary modules installed.
|
||||
|
||||
### **ThunderHub - Server**
|
||||
|
||||
To be able to use the HodlHodl integration create a `.env` file in the `/server` folder with `HODL_KEY='[YOUR API KEY]'` and replace `[YOUR API KEY]` with the one that HodlHodl provides you.
|
||||
|
||||
#### To get the server running use the following commands
|
||||
|
||||
```javascript
|
||||
yarn server:prod
|
||||
yarn server:run
|
||||
```
|
||||
|
||||
If the server starts succesfully, you should see `info [server.js]: Server ready at http://localhost:3001/` in the terminal
|
||||
|
||||
### **ThunderHub - Client**
|
||||
|
||||
#### To get the React frontend running use the following commands
|
||||
|
||||
##### This must be done in the `/client` folder
|
||||
After `yarn` has finished installing all the dependencies you can proceed to build and run the app with the following commands.
|
||||
|
||||
```javascript
|
||||
yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
If the frontend starts succesfully, you should see `Compiled successfully! You can now view app in the browser.` in the terminal and a browser window should have opened in your browser.
|
||||
This will start the server on port 3000, so just head to `localhost:3000` to see the app running.
|
||||
|
||||
#### HodlHodl Integration
|
||||
|
||||
To be able to use the HodlHodl integration create a `.env` file in the root folder with `HODL_KEY='[YOUR API KEY]'` and replace `[YOUR API KEY]` with the one that HodlHodl provides you.
|
||||
|
||||
## Development
|
||||
|
||||
If you want to develop on ThunderHub and want hot reloading when you do changes, use the following commands:
|
||||
|
||||
### ThunderHub - Server
|
||||
|
||||
```javascript
|
||||
yarn server:dev
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### ThunderHub - Client
|
||||
|
||||
Running the commands `yarn start` in the `client` folder works for development.
|
||||
|
||||
#### Storybook
|
||||
|
||||
You can also get storybook running for quicker component development.
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
build
|
|
@ -1 +0,0 @@
|
|||
REACT_APP_VERSION=$npm_package_version
|
27
client/.gitignore
vendored
27
client/.gitignore
vendored
|
@ -1,27 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
#webpack
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
module.exports = {
|
||||
stories: ['../src/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-knobs/register',
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/preset-create-react-app',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-viewport/register',
|
||||
],
|
||||
webpackFinal: async (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('awesome-typescript-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('react-docgen-typescript-loader'),
|
||||
},
|
||||
],
|
||||
});
|
||||
config.resolve.extensions.push('.ts', '.tsx');
|
||||
return config;
|
||||
},
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
import { addDecorator, addParameters } from '@storybook/react';
|
||||
import themeDecorator from './themeDecorator';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||
|
||||
const customViewports = {
|
||||
smallScreen: {
|
||||
name: 'Small Screen',
|
||||
styles: {
|
||||
width: '578px',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
addParameters({
|
||||
viewport: {
|
||||
viewports: { ...INITIAL_VIEWPORTS, ...customViewports },
|
||||
},
|
||||
});
|
||||
addDecorator(themeDecorator);
|
||||
addDecorator(withKnobs);
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { ThemeProvider, css } from 'styled-components';
|
||||
import { select, boolean } from '@storybook/addon-knobs';
|
||||
import { backgroundColor, cardColor } from '../src/styles/Themes';
|
||||
|
||||
const StyledBackground = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 100px 0;
|
||||
${({ withBackground, cardBackground }) =>
|
||||
withBackground &&
|
||||
css`
|
||||
background: ${cardBackground ? cardColor : backgroundColor};
|
||||
`}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ThemeDecorator = (storyFn) => {
|
||||
const background = boolean('No Background', false);
|
||||
const cardBackground = boolean('Card Background', true);
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={{ mode: select('Theme', ['dark', 'light'], 'dark') }}
|
||||
>
|
||||
<StyledBackground
|
||||
withBackground={!background}
|
||||
cardBackground={cardBackground}
|
||||
>
|
||||
{storyFn()}
|
||||
</StyledBackground>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeDecorator;
|
|
@ -1,14 +0,0 @@
|
|||
FROM node:11-alpine as build
|
||||
|
||||
WORKDIR /usr/src/client
|
||||
|
||||
COPY package.json /usr/src/client
|
||||
COPY yarn.lock /usr/src/client
|
||||
RUN yarn install --production=true
|
||||
|
||||
COPY . /usr/src/client
|
||||
RUN yarn build
|
||||
|
||||
RUN yarn global add serve
|
||||
|
||||
CMD ["serve", "-s", "build"]
|
|
@ -1,15 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
{
|
||||
"name": "@thunderhub/client",
|
||||
"version": "0.2.1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"build-storybook": "build-storybook -s public",
|
||||
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client",
|
||||
"precommit": "pretty-quick --staged"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/apotdevin/thunderhub.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "apotdevin",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@types/crypto-js": "^3.1.44",
|
||||
"@types/jest": "25.1.5",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.sortby": "^4.7.6",
|
||||
"@types/node": "13.11.0",
|
||||
"@types/numeral": "^0.0.26",
|
||||
"@types/qrcode.react": "^1.0.0",
|
||||
"@types/react": "16.9.32",
|
||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||
"@types/react-dom": "16.9.6",
|
||||
"@types/react-modal": "^3.10.5",
|
||||
"@types/react-qr-reader": "^2.1.2",
|
||||
"@types/react-router-dom": "^5.1.2",
|
||||
"@types/react-tooltip": "^3.11.0",
|
||||
"@types/styled-components": "^5.0.1",
|
||||
"@types/styled-react-modal": "^1.2.0",
|
||||
"@types/styled-theming": "^2.2.2",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"@types/victory": "^33.1.4",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"apollo-boost": "^0.4.4",
|
||||
"crypto-js": "^4.0.0",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"numeral": "^2.0.6",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"qs": "^6.9.3",
|
||||
"react": "^16.13.0",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-intersection-observer": "^8.26.1",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-spinners": "^0.8.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-toastify": "^5.4.1",
|
||||
"react-tooltip": "^4.1.3",
|
||||
"snyk": "^1.305.0",
|
||||
"styled-components": "^5.0.1",
|
||||
"styled-react-modal": "^2.0.0",
|
||||
"styled-theming": "^2.2.0",
|
||||
"typescript": "^3.8.3",
|
||||
"uuid": "^7.0.3",
|
||||
"victory": "^34.1.3",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-info": "^5.3.18",
|
||||
"@storybook/addon-knobs": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-viewport": "^5.3.18",
|
||||
"@storybook/addons": "^5.3.18",
|
||||
"@storybook/preset-create-react-app": "^2.1.1",
|
||||
"@storybook/react": "^5.3.18",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"react-docgen-typescript-loader": "^3.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -1,60 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
property="og:description"
|
||||
content="Manage and monitor your lightning network node right inside your browser"
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content="ThunderHub - Lightning Node Manager"
|
||||
/>
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon-152x152.png" />
|
||||
<link rel="canonical" href="https://thunderhub.io/" />
|
||||
<meta property="og:url" content="https://thunderhub.io" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="ThunderHub - Lightning Node Manager"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Manage and monitor your lightning network node right inside your browser"
|
||||
/>
|
||||
<meta name="twitter:site" content="@thunderhubio" />
|
||||
<meta name="twitter:creator" content="@thunderhubio" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>ThunderHub - Lightning Node Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"short_name": "ThunderHub",
|
||||
"name": "ThunderHub - Lightning Node Manager",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "apple-touch-icon-152x152.png",
|
||||
"type": "image/png",
|
||||
"sizes": "152x152"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
|
@ -1,80 +0,0 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { GlobalStyles } from './styles/GlobalStyle';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import ApolloClient from 'apollo-boost';
|
||||
import { useSettings } from './context/SettingsContext';
|
||||
import { ModalProvider } from 'styled-react-modal';
|
||||
import { useAccount } from './context/AccountContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Header } from './sections/header/Header';
|
||||
import { Footer } from './sections/footer/Footer';
|
||||
import { LoadingCard } from './components/loading/LoadingCard';
|
||||
import { ScrollToTop } from 'components/scrollToTop/ScrollToTop';
|
||||
import { ContextProvider } from 'context/ContextProvider';
|
||||
import { ConnectionCheck } from 'components/connectionCheck/ConnectionCheck';
|
||||
import { StatusCheck } from 'components/statusCheck/StatusCheck';
|
||||
import { BaseModalBackground } from 'styled-react-modal';
|
||||
|
||||
const EntryView = React.lazy(() => import('./views/entry/Entry'));
|
||||
const ContentView = React.lazy(() => import('./sections/content/Content'));
|
||||
|
||||
toast.configure({ draggable: false });
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri:
|
||||
process.env.REACT_APP_API_URL ?? process.env.NODE_ENV === 'production'
|
||||
? 'https://api.thunderhub.io'
|
||||
: 'http://localhost:3001',
|
||||
});
|
||||
|
||||
const ContextApp: React.FC = () => {
|
||||
const { theme } = useSettings();
|
||||
const { loggedIn, admin, viewOnly, sessionAdmin } = useAccount();
|
||||
|
||||
const renderContent = () => (
|
||||
<Suspense
|
||||
fallback={<LoadingCard noCard={true} loadingHeight={'240px'} />}
|
||||
>
|
||||
{!loggedIn && admin === '' ? (
|
||||
<EntryView />
|
||||
) : admin !== '' && viewOnly === '' && sessionAdmin === '' ? (
|
||||
<EntryView session={true} />
|
||||
) : (
|
||||
<>
|
||||
<ConnectionCheck />
|
||||
<StatusCheck />
|
||||
<ContentView />
|
||||
</>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={{ mode: theme }}>
|
||||
<ModalProvider backgroundComponent={BaseModalBackground}>
|
||||
<ScrollToTop />
|
||||
<GlobalStyles />
|
||||
<Header />
|
||||
{renderContent()}
|
||||
<Footer />
|
||||
</ModalProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ApolloProvider client={client}>
|
||||
<ContextProvider>
|
||||
<ContextApp />
|
||||
</ContextProvider>
|
||||
</ApolloProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import { getValue } from '../../helpers/Helpers';
|
||||
import { useSettings } from '../../context/SettingsContext';
|
||||
import { usePriceState } from '../../context/PriceContext';
|
||||
|
||||
type PriceProps = {
|
||||
price: number;
|
||||
symbol: string;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
type AnimatedProps = {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export const AnimatedNumber = ({ amount }: AnimatedProps) => {
|
||||
const { value } = useSpring({
|
||||
from: { value: 0 },
|
||||
value: amount,
|
||||
});
|
||||
const { currency } = useSettings();
|
||||
const { prices } = usePriceState();
|
||||
|
||||
let priceProps: PriceProps = {
|
||||
price: 0,
|
||||
symbol: '',
|
||||
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
|
||||
};
|
||||
|
||||
if (prices) {
|
||||
const current: { last: number; symbol: string } = prices[currency] ?? {
|
||||
last: 0,
|
||||
symbol: '',
|
||||
};
|
||||
|
||||
priceProps = {
|
||||
price: current.last,
|
||||
symbol: current.symbol,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<animated.div>
|
||||
{value.interpolate((amount) => getValue({ amount, ...priceProps }))}
|
||||
</animated.div>
|
||||
);
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { Sub4Title } from '../generic/Styled';
|
||||
import { fontColors, textColor } from 'styles/Themes';
|
||||
|
||||
export const Line = styled.div`
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
export const StyledTitle = styled(Sub4Title)`
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
margin-bottom: 0px;
|
||||
`;
|
||||
|
||||
export const CheckboxText = styled.div`
|
||||
font-size: 13px;
|
||||
color: ${fontColors.grey7};
|
||||
text-align: justify;
|
||||
`;
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
color: ${textColor};
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
margin: 32px 0 8px;
|
||||
`;
|
||||
|
||||
export const FixedWidth = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin: 0px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
export const QRTextWrapper = styled.div`
|
||||
display: flex;
|
||||
margin: 16px 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import { GET_CAN_ADMIN } from 'graphql/query';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { SingleLine, Sub4Title } from 'components/generic/Styled';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { themeColors } from 'styles/Themes';
|
||||
import { XSvg, Check } from 'components/generic/Icons';
|
||||
|
||||
type AdminProps = {
|
||||
host: string;
|
||||
admin: string;
|
||||
cert?: string;
|
||||
setChecked: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const AdminCheck = ({ host, admin, cert, setChecked }: AdminProps) => {
|
||||
const { data, loading } = useQuery(GET_CAN_ADMIN, {
|
||||
skip: !admin,
|
||||
variables: { auth: { host, macaroon: admin, cert } },
|
||||
onError: () => {
|
||||
setChecked(false);
|
||||
},
|
||||
onCompleted: () => {
|
||||
setChecked(true);
|
||||
},
|
||||
});
|
||||
|
||||
const content = () => {
|
||||
if (loading) {
|
||||
return <ScaleLoader height={20} color={themeColors.blue3} />;
|
||||
} else if (data?.adminCheck) {
|
||||
return <Check />;
|
||||
}
|
||||
return <XSvg />;
|
||||
};
|
||||
|
||||
return (
|
||||
<SingleLine>
|
||||
<Sub4Title>Admin Macaroon</Sub4Title>
|
||||
{content()}
|
||||
</SingleLine>
|
||||
);
|
||||
};
|
|
@ -1,159 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_CAN_CONNECT } from 'graphql/query';
|
||||
import { SingleLine, Sub4Title, Separation } from 'components/generic/Styled';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { themeColors } from 'styles/Themes';
|
||||
import { Check, XSvg } from 'components/generic/Icons';
|
||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||
import { AdminCheck } from './AdminCheck';
|
||||
import { Text } from 'views/other/OtherViews.styled';
|
||||
|
||||
type ViewProps = {
|
||||
host: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
adminChecked: boolean;
|
||||
callback: () => void;
|
||||
setAdminChecked: (state: boolean) => void;
|
||||
handleConnect: () => void;
|
||||
setName: (name: string) => void;
|
||||
};
|
||||
|
||||
export const ViewCheck = ({
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
adminChecked,
|
||||
setAdminChecked,
|
||||
handleConnect,
|
||||
callback,
|
||||
setName,
|
||||
}: ViewProps) => {
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
|
||||
const { data, loading } = useQuery(GET_CAN_CONNECT, {
|
||||
variables: { auth: { host, macaroon: viewOnly ?? admin ?? '', cert } },
|
||||
onCompleted: () => setConfirmed(true),
|
||||
onError: () => setConfirmed(false),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && data.getNodeInfo) {
|
||||
setName(data.getNodeInfo.alias);
|
||||
}
|
||||
}, [loading, data, setName]);
|
||||
|
||||
const content = () => {
|
||||
if (loading) {
|
||||
return <ScaleLoader height={20} color={themeColors.blue3} />;
|
||||
} else if (data?.getNodeInfo.alias && viewOnly) {
|
||||
return <Check />;
|
||||
}
|
||||
return <XSvg />;
|
||||
};
|
||||
|
||||
const renderInfo = () => {
|
||||
if (!loading && data && data.getNodeInfo) {
|
||||
return (
|
||||
<>
|
||||
<SingleLine>
|
||||
<Sub4Title>Alias</Sub4Title>
|
||||
<Sub4Title>{data.getNodeInfo.alias}</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Synced To Chain</Sub4Title>
|
||||
<Sub4Title>
|
||||
{data.getNodeInfo.is_synced_to_chain ? 'Yes' : 'No'}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Version</Sub4Title>
|
||||
<Sub4Title>
|
||||
{data.getNodeInfo.version.split(' ')[0]}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Active Channels</Sub4Title>
|
||||
<Sub4Title>
|
||||
{data.getNodeInfo.active_channels_count}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Pending Channels</Sub4Title>
|
||||
<Sub4Title>
|
||||
{data.getNodeInfo.pending_channels_count}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Closed Channels</Sub4Title>
|
||||
<Sub4Title>
|
||||
{data.getNodeInfo.closed_channels_count}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<Separation />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!confirmed) {
|
||||
return 'Go Back';
|
||||
} else if (adminChecked && !viewOnly && admin) {
|
||||
return 'Connect (Admin-Only)';
|
||||
} else if (!adminChecked && viewOnly) {
|
||||
return 'Connect (View-Only)';
|
||||
} else {
|
||||
return 'Connect';
|
||||
}
|
||||
};
|
||||
|
||||
const renderButton = () => (
|
||||
<ColorButton
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
arrow={confirmed}
|
||||
onClick={() => {
|
||||
if (confirmed) {
|
||||
handleConnect();
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderTitle()}
|
||||
</ColorButton>
|
||||
);
|
||||
|
||||
const renderText = () => (
|
||||
<Text>
|
||||
Failed to connect to node. Please verify the information provided.
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderInfo()}
|
||||
{!confirmed && !loading && renderText()}
|
||||
<SingleLine>
|
||||
<Sub4Title>View-Only Macaroon</Sub4Title>
|
||||
{content()}
|
||||
</SingleLine>
|
||||
{admin && (
|
||||
<AdminCheck
|
||||
host={host}
|
||||
admin={admin}
|
||||
cert={cert}
|
||||
setChecked={setAdminChecked}
|
||||
/>
|
||||
)}
|
||||
{renderButton()}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,188 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { LoginForm } from './views/NormalLogin';
|
||||
import { ConnectLoginForm } from './views/ConnectLogin';
|
||||
import { BTCLoginForm } from './views/BTCLogin';
|
||||
import { QRLogin } from './views/QRLogin';
|
||||
import { ViewCheck } from './checks/ViewCheck';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { saveUserAuth, getAccountId } from 'utils/auth';
|
||||
import { PasswordInput } from './views/Password';
|
||||
import { useConnectionDispatch } from 'context/ConnectionContext';
|
||||
import { useStatusDispatch } from 'context/StatusContext';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
type AuthProps = {
|
||||
type: string;
|
||||
status: string;
|
||||
callback: () => void;
|
||||
setStatus: (state: string) => void;
|
||||
};
|
||||
|
||||
export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
|
||||
const { changeAccount, accounts } = useAccount();
|
||||
const { push } = useHistory();
|
||||
|
||||
const dispatch = useConnectionDispatch();
|
||||
const dispatchState = useStatusDispatch();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
const [host, setHost] = useState<string>();
|
||||
const [admin, setAdmin] = useState<string>();
|
||||
const [viewOnly, setViewOnly] = useState<string>();
|
||||
const [cert, setCert] = useState<string>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
|
||||
const [adminChecked, setAdminChecked] = useState(false);
|
||||
|
||||
const handleSet = ({
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
skipCheck,
|
||||
}: {
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
skipCheck?: boolean;
|
||||
}) => {
|
||||
const id = getAccountId(
|
||||
host ?? '',
|
||||
viewOnly ?? '',
|
||||
admin ?? '',
|
||||
cert ?? '',
|
||||
);
|
||||
|
||||
const accountExists =
|
||||
accounts.findIndex((account) => account.id === id) > -1;
|
||||
|
||||
if (accountExists) {
|
||||
toast.error('Account already exists.');
|
||||
} else if (!host) {
|
||||
toast.error('A host url is needed to connect.');
|
||||
} else if (!admin && !viewOnly) {
|
||||
toast.error('View-Only or Admin macaroon are needed to connect.');
|
||||
} else if (skipCheck) {
|
||||
quickSave({ name, cert, admin, viewOnly, host });
|
||||
} else {
|
||||
host && setHost(host);
|
||||
admin && setAdmin(admin);
|
||||
viewOnly && setViewOnly(viewOnly);
|
||||
cert && setCert(cert);
|
||||
|
||||
setStatus('confirmNode');
|
||||
}
|
||||
};
|
||||
|
||||
const quickSave = ({
|
||||
name = 'Unknown',
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
}: {
|
||||
name?: string;
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
}) => {
|
||||
saveUserAuth({
|
||||
name,
|
||||
host: host || '',
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const id = getAccountId(host, viewOnly, admin, cert);
|
||||
|
||||
dispatch({ type: 'disconnected' });
|
||||
dispatchState({ type: 'disconnected' });
|
||||
changeAccount(id);
|
||||
|
||||
push('/');
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!host) {
|
||||
toast.error('A host url is needed to connect.');
|
||||
} else if (!admin && !viewOnly) {
|
||||
toast.error('View-Only or Admin macaroon are needed to connect.');
|
||||
} else {
|
||||
const encryptedAdmin =
|
||||
admin && password
|
||||
? CryptoJS.AES.encrypt(admin, password).toString()
|
||||
: undefined;
|
||||
|
||||
saveUserAuth({
|
||||
name,
|
||||
host,
|
||||
admin: encryptedAdmin,
|
||||
viewOnly,
|
||||
cert,
|
||||
accounts,
|
||||
});
|
||||
|
||||
const id = getAccountId(host, viewOnly, admin, cert);
|
||||
|
||||
dispatch({ type: 'disconnected' });
|
||||
dispatchState({ type: 'disconnected' });
|
||||
changeAccount(id);
|
||||
|
||||
push('/');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
if (adminChecked) {
|
||||
setStatus('password');
|
||||
} else {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
const renderView = () => {
|
||||
switch (type) {
|
||||
case 'login':
|
||||
return <LoginForm handleSet={handleSet} />;
|
||||
case 'qrcode':
|
||||
return <QRLogin handleSet={handleSet} />;
|
||||
case 'connect':
|
||||
return <ConnectLoginForm handleSet={handleSet} />;
|
||||
default:
|
||||
return <BTCLoginForm handleSet={handleSet} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === 'none' && renderView()}
|
||||
{status === 'confirmNode' && host && (
|
||||
<ViewCheck
|
||||
host={host}
|
||||
admin={admin}
|
||||
viewOnly={viewOnly}
|
||||
cert={cert}
|
||||
adminChecked={adminChecked}
|
||||
setAdminChecked={setAdminChecked}
|
||||
handleConnect={handleConnect}
|
||||
callback={callback}
|
||||
setName={setName}
|
||||
/>
|
||||
)}
|
||||
{status === 'password' && (
|
||||
<PasswordInput
|
||||
isPass={password}
|
||||
setPass={setPassword}
|
||||
callback={handleSave}
|
||||
loading={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { getConfigLnd } from '../../../utils/auth';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Input } from 'components/input/Input';
|
||||
import { Line, StyledTitle } from '../Auth.styled';
|
||||
import { RiskCheckboxAndConfirm } from './Checkboxes';
|
||||
|
||||
interface AuthProps {
|
||||
handleSet: ({
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
}: {
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const BTCLoginForm = ({ handleSet }: AuthProps) => {
|
||||
const [json, setJson] = useState('');
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
try {
|
||||
JSON.parse(json);
|
||||
const { cert, admin, viewOnly, host } = getConfigLnd(json);
|
||||
handleSet({ host, admin, viewOnly, cert });
|
||||
} catch (error) {
|
||||
toast.error('Invalid JSON');
|
||||
}
|
||||
};
|
||||
|
||||
const canConnect = json !== '' && checked;
|
||||
return (
|
||||
<>
|
||||
<Line>
|
||||
<StyledTitle>BTCPayServer Connect JSON:</StyledTitle>
|
||||
<Input onChange={(e) => setJson(e.target.value)} />
|
||||
</Line>
|
||||
<RiskCheckboxAndConfirm
|
||||
disabled={!canConnect}
|
||||
handleClick={handleClick}
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Checkbox } from 'components/checkbox/Checkbox';
|
||||
import { CheckboxText, StyledContainer, FixedWidth } from '../Auth.styled';
|
||||
import { AlertCircle } from 'components/generic/Icons';
|
||||
import { fontColors } from 'styles/Themes';
|
||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||
|
||||
type CheckboxProps = {
|
||||
handleClick: () => void;
|
||||
disabled: boolean;
|
||||
checked: boolean;
|
||||
onChange: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const RiskCheckboxAndConfirm = ({
|
||||
handleClick,
|
||||
disabled,
|
||||
checked,
|
||||
onChange,
|
||||
}: CheckboxProps) => (
|
||||
<>
|
||||
<Checkbox checked={checked} onChange={onChange}>
|
||||
<CheckboxText>
|
||||
I'm feeling reckless - I understand that Lightning, LND and
|
||||
ThunderHub are under constant development and that there is
|
||||
always a risk of losing funds.
|
||||
</CheckboxText>
|
||||
</Checkbox>
|
||||
<ColorButton
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
withMargin={'32px 0 0'}
|
||||
fullWidth={true}
|
||||
arrow={true}
|
||||
>
|
||||
Connect
|
||||
</ColorButton>
|
||||
<WarningBox />
|
||||
</>
|
||||
);
|
||||
|
||||
export const WarningBox = () => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<FixedWidth>
|
||||
<AlertCircle color={fontColors.grey7} />
|
||||
</FixedWidth>
|
||||
<CheckboxText>
|
||||
Macaroons are handled by the ThunderHub server to connect to
|
||||
your LND node but are never stored. Still, this involves a
|
||||
certain degree of trust you must be aware of.
|
||||
</CheckboxText>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { getAuthLnd, getBase64CertfromDerFormat } from '../../../utils/auth';
|
||||
import { Input } from 'components/input/Input';
|
||||
import { Line, StyledTitle } from '../Auth.styled';
|
||||
import { RiskCheckboxAndConfirm } from './Checkboxes';
|
||||
|
||||
interface AuthProps {
|
||||
handleSet: ({
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
}: {
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const ConnectLoginForm = ({ handleSet }: AuthProps) => {
|
||||
const [url, setUrl] = useState('');
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
const { cert, macaroon, socket } = getAuthLnd(url);
|
||||
const base64Cert = getBase64CertfromDerFormat(cert) || '';
|
||||
|
||||
handleSet({
|
||||
host: socket,
|
||||
admin: macaroon,
|
||||
cert: base64Cert,
|
||||
});
|
||||
};
|
||||
|
||||
const canConnect = url !== '' && checked;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Line>
|
||||
<StyledTitle>LND Connect Url:</StyledTitle>
|
||||
<Input onChange={(e) => setUrl(e.target.value)} />
|
||||
</Line>
|
||||
<RiskCheckboxAndConfirm
|
||||
disabled={!canConnect}
|
||||
handleClick={handleClick}
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,97 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Input } from 'components/input/Input';
|
||||
import { Line, StyledTitle } from '../Auth.styled';
|
||||
import { SingleLine, Sub4Title } from 'components/generic/Styled';
|
||||
import {
|
||||
MultiButton,
|
||||
SingleButton,
|
||||
} from 'components/buttons/multiButton/MultiButton';
|
||||
import { RiskCheckboxAndConfirm } from './Checkboxes';
|
||||
|
||||
interface AuthProps {
|
||||
handleSet: ({
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
}: {
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const LoginForm = ({ handleSet }: AuthProps) => {
|
||||
const [isViewOnly, setIsViewOnly] = useState(true);
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const [host, setHost] = useState('');
|
||||
const [admin, setAdmin] = useState('');
|
||||
const [viewOnly, setRead] = useState('');
|
||||
const [cert, setCert] = useState('');
|
||||
|
||||
const handleClick = () => {
|
||||
handleSet({ host, admin, viewOnly, cert });
|
||||
};
|
||||
|
||||
const canConnect =
|
||||
host !== '' && (admin !== '' || viewOnly !== '') && checked;
|
||||
return (
|
||||
<>
|
||||
<SingleLine>
|
||||
<Sub4Title>Type of Account:</Sub4Title>
|
||||
<MultiButton>
|
||||
<SingleButton
|
||||
selected={isViewOnly}
|
||||
onClick={() => setIsViewOnly(true)}
|
||||
>
|
||||
ViewOnly
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
selected={!isViewOnly}
|
||||
onClick={() => setIsViewOnly(false)}
|
||||
>
|
||||
Admin
|
||||
</SingleButton>
|
||||
</MultiButton>
|
||||
</SingleLine>
|
||||
<Line>
|
||||
<StyledTitle>Host:</StyledTitle>
|
||||
<Input
|
||||
placeholder={'Url and port (e.g.: www.node.com:443)'}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
/>
|
||||
</Line>
|
||||
{!isViewOnly && (
|
||||
<Line>
|
||||
<StyledTitle>Admin:</StyledTitle>
|
||||
<Input
|
||||
placeholder={'Base64 or HEX Admin macaroon'}
|
||||
onChange={(e) => setAdmin(e.target.value)}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
<Line>
|
||||
<StyledTitle>Readonly:</StyledTitle>
|
||||
<Input
|
||||
placeholder={'Base64 or HEX Readonly macaroon'}
|
||||
onChange={(e) => setRead(e.target.value)}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<StyledTitle>Certificate:</StyledTitle>
|
||||
<Input
|
||||
placeholder={'Base64 or HEX TLS Certificate'}
|
||||
onChange={(e) => setCert(e.target.value)}
|
||||
/>
|
||||
</Line>
|
||||
<RiskCheckboxAndConfirm
|
||||
disabled={!canConnect}
|
||||
handleClick={handleClick}
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Sub4Title, SubTitle } from '../../generic/Styled';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
import { ColorButton } from '../../buttons/colorButton/ColorButton';
|
||||
import { Input } from 'components/input/Input';
|
||||
import { Line } from '../Auth.styled';
|
||||
import { LoadingBar } from 'components/loadingBar/LoadingBar';
|
||||
|
||||
interface PasswordProps {
|
||||
isPass?: string;
|
||||
setPass: (pass: string) => void;
|
||||
callback: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const PasswordInput = ({
|
||||
isPass = '',
|
||||
setPass,
|
||||
callback,
|
||||
loading = false,
|
||||
}: PasswordProps) => {
|
||||
const strength = (100 * Math.min(zxcvbn(isPass).guesses_log10, 40)) / 40;
|
||||
const needed = process.env.NODE_ENV === 'development' ? 1 : 20;
|
||||
return (
|
||||
<>
|
||||
<SubTitle>Please Input a Password</SubTitle>
|
||||
<Line>
|
||||
<Sub4Title>Password:</Sub4Title>
|
||||
<Input onChange={(e) => setPass(e.target.value)} />
|
||||
</Line>
|
||||
<Line>
|
||||
<Sub4Title>Strength:</Sub4Title>
|
||||
<LoadingBar percent={strength} />
|
||||
</Line>
|
||||
<ColorButton
|
||||
disabled={strength < needed}
|
||||
onClick={callback}
|
||||
withMargin={'32px 0 0'}
|
||||
fullWidth={true}
|
||||
arrow={true}
|
||||
loading={loading}
|
||||
>
|
||||
Connect
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,163 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import QrReader from 'react-qr-reader';
|
||||
import Modal from '../../../components/modal/ReactModal';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getQRConfig } from 'utils/auth';
|
||||
import { Line, QRTextWrapper } from '../Auth.styled';
|
||||
import sortBy from 'lodash.sortby';
|
||||
import { LoadingBar } from 'components/loadingBar/LoadingBar';
|
||||
import { SubTitle } from 'components/generic/Styled';
|
||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||
|
||||
type QRLoginProps = {
|
||||
handleSet: ({
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
skipCheck,
|
||||
}: {
|
||||
name?: string;
|
||||
host?: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
skipCheck?: boolean;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export const QRLogin = ({ handleSet }: QRLoginProps) => {
|
||||
const [qrData, setQrData] = useState<any>([]);
|
||||
const [modalOpen, setModalOpen] = useState(true);
|
||||
const [modalClosed, setModalClosed] = useState('none');
|
||||
|
||||
const [total, setTotal] = useState(0);
|
||||
const [missing, setMissing] = useState<number[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (qrData.length >= total && total !== 0) {
|
||||
setModalOpen(false);
|
||||
|
||||
const sorted = sortBy(qrData, 'index');
|
||||
const strings = sorted.map((code: { auth: string }) => code.auth);
|
||||
const completeString = strings.join('');
|
||||
|
||||
try {
|
||||
const { name, cert, admin, viewOnly, host } = getQRConfig(
|
||||
completeString,
|
||||
);
|
||||
|
||||
handleSet({
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
skipCheck: true,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('Error reading QR codes.');
|
||||
}
|
||||
}
|
||||
}, [qrData, handleSet, total]);
|
||||
|
||||
const handleScan = (data: string | null) => {
|
||||
if (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
!total && setTotal(parsed.total);
|
||||
!missing && setMissing([...Array(parsed.total).keys()]);
|
||||
|
||||
if (
|
||||
missing &&
|
||||
missing.length >= 0 &&
|
||||
missing.includes(parsed.index)
|
||||
) {
|
||||
const remaining = missing.filter((value: number) => {
|
||||
const number = parseInt(parsed.index);
|
||||
return value !== number;
|
||||
});
|
||||
const data = [...qrData, parsed];
|
||||
setQrData(data);
|
||||
setMissing(remaining);
|
||||
}
|
||||
} catch (error) {
|
||||
setModalOpen(false);
|
||||
toast.error('Error reading QR codes.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setModalOpen(false);
|
||||
setModalClosed('error');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setModalClosed('forced');
|
||||
setModalOpen(false);
|
||||
setMissing(undefined);
|
||||
setTotal(0);
|
||||
setQrData([]);
|
||||
};
|
||||
|
||||
const renderInfo = () => {
|
||||
switch (modalClosed) {
|
||||
case 'forced':
|
||||
return (
|
||||
<>
|
||||
<QRTextWrapper>
|
||||
<SubTitle>
|
||||
No information read from QR Codes.
|
||||
</SubTitle>
|
||||
</QRTextWrapper>
|
||||
<ColorButton
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
setModalClosed('none');
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<QRTextWrapper>
|
||||
<SubTitle>
|
||||
Make sure you have a camara available and that you
|
||||
have given ThunderHub the correct permissions to use
|
||||
it.
|
||||
</SubTitle>
|
||||
</QRTextWrapper>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderInfo()}
|
||||
<Modal isOpen={modalOpen} closeCallback={handleClose}>
|
||||
<Line>
|
||||
<LoadingBar
|
||||
percent={
|
||||
missing
|
||||
? 100 * ((total - missing.length) / total)
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
<QrReader
|
||||
delay={500}
|
||||
onError={handleError}
|
||||
onScan={handleScan}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_BITCOIN_FEES } from '../../graphql/query';
|
||||
import { useBitcoinDispatch } from '../../context/BitcoinContext';
|
||||
|
||||
export const BitcoinFees = () => {
|
||||
const setInfo = useBitcoinDispatch();
|
||||
const { loading, data, stopPolling } = useQuery(GET_BITCOIN_FEES, {
|
||||
onError: () => {
|
||||
setInfo({ type: 'error' });
|
||||
stopPolling();
|
||||
},
|
||||
pollInterval: 60000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && data.getBitcoinFees) {
|
||||
const { fast, halfHour, hour } = data.getBitcoinFees;
|
||||
setInfo({
|
||||
type: 'fetched',
|
||||
state: { loading: false, error: false, fast, halfHour, hour },
|
||||
});
|
||||
}
|
||||
}, [data, loading, setInfo]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_BITCOIN_PRICE } from '../../graphql/query';
|
||||
import { usePriceDispatch } from '../../context/PriceContext';
|
||||
|
||||
export const BitcoinPrice = () => {
|
||||
const setPrices = usePriceDispatch();
|
||||
const { loading, data, stopPolling } = useQuery(GET_BITCOIN_PRICE, {
|
||||
onError: () => setPrices({ type: 'error' }),
|
||||
pollInterval: 60000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && data.getBitcoinPrice) {
|
||||
try {
|
||||
const prices = JSON.parse(data.getBitcoinPrice);
|
||||
setPrices({
|
||||
type: 'fetched',
|
||||
state: { loading: false, error: false, prices },
|
||||
});
|
||||
} catch (error) {
|
||||
stopPolling();
|
||||
setPrices({ type: 'error' });
|
||||
}
|
||||
}
|
||||
}, [data, loading, setPrices, stopPolling]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { burgerColor } from 'styles/Themes';
|
||||
import { NodeInfo } from 'sections/navigation/nodeInfo/NodeInfo';
|
||||
import { Navigation } from 'sections/navigation/Navigation';
|
||||
import { SideSettings } from 'sections/navigation/sideSettings/SideSettings';
|
||||
|
||||
const StyledBurger = styled.div`
|
||||
padding: 16px 16px 0;
|
||||
background-color: ${burgerColor};
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
${({ open }: { open: boolean }) =>
|
||||
open &&
|
||||
css`
|
||||
margin-bottom: 16px;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface BurgerProps {
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export const BurgerMenu = ({ open, setOpen }: BurgerProps) => {
|
||||
return (
|
||||
<StyledBurger open={open}>
|
||||
<NodeInfo isBurger={true} />
|
||||
<SideSettings isBurger={true} />
|
||||
<Navigation isBurger={true} setOpen={setOpen} />
|
||||
</StyledBurger>
|
||||
);
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ColorButton } from './ColorButton';
|
||||
import { text, boolean, color } from '@storybook/addon-knobs';
|
||||
|
||||
export default {
|
||||
title: 'Color Button',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const withColor = boolean('With Color', false);
|
||||
|
||||
const buttonColor = withColor ? { color: color('Color', 'yellow') } : {};
|
||||
|
||||
return (
|
||||
<ColorButton
|
||||
{...buttonColor}
|
||||
loading={boolean('Loading', false)}
|
||||
disabled={boolean('Disabled', false)}
|
||||
arrow={boolean('With Arrow', false)}
|
||||
selected={boolean('Selected', false)}
|
||||
onClick={action('clicked')}
|
||||
withMargin={text('Margin', '')}
|
||||
withBorder={boolean('With Border', false)}
|
||||
fullWidth={boolean('Full Width', false)}
|
||||
width={text('Width', '')}
|
||||
>
|
||||
{text('Text', 'Button')}
|
||||
</ColorButton>
|
||||
);
|
||||
};
|
|
@ -1,159 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
textColor,
|
||||
colorButtonBackground,
|
||||
disabledButtonBackground,
|
||||
disabledButtonBorder,
|
||||
disabledTextColor,
|
||||
colorButtonBorder,
|
||||
colorButtonBorderTwo,
|
||||
hoverTextColor,
|
||||
} from '../../../styles/Themes';
|
||||
import { ChevronRight } from '../../generic/Icons';
|
||||
import { themeColors } from '../../../styles/Themes';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
|
||||
interface GeneralProps {
|
||||
fullWidth?: boolean;
|
||||
buttonWidth?: string;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
const GeneralButton = styled.button`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
|
||||
width: ${({ fullWidth, buttonWidth }: GeneralProps) =>
|
||||
fullWidth ? '100%' : buttonWidth ? buttonWidth : 'auto'};
|
||||
`;
|
||||
|
||||
const StyledArrow = styled.div`
|
||||
margin: 0 -8px -5px 4px;
|
||||
`;
|
||||
|
||||
interface BorderProps {
|
||||
borderColor?: string;
|
||||
selected?: boolean;
|
||||
withBorder?: boolean;
|
||||
}
|
||||
|
||||
const BorderButton = styled(GeneralButton)`
|
||||
${({ selected }) => selected && `cursor: default`};
|
||||
${({ selected }) => selected && `font-weight: 900`};
|
||||
background-color: ${colorButtonBackground};
|
||||
color: ${textColor};
|
||||
border: 1px solid
|
||||
${({ borderColor, selected, withBorder }: BorderProps) =>
|
||||
withBorder
|
||||
? borderColor
|
||||
? borderColor
|
||||
: colorButtonBorder
|
||||
: selected
|
||||
? colorButtonBorder
|
||||
: colorButtonBorderTwo};
|
||||
|
||||
&:hover {
|
||||
${({ borderColor, selected }: BorderProps) =>
|
||||
!selected
|
||||
? css`
|
||||
border: 1px solid ${colorButtonBackground};
|
||||
background-color: ${borderColor
|
||||
? borderColor
|
||||
: colorButtonBorder};
|
||||
color: ${hoverTextColor};
|
||||
`
|
||||
: ''};
|
||||
}
|
||||
`;
|
||||
|
||||
const DisabledButton = styled(GeneralButton)`
|
||||
border: none;
|
||||
background-color: ${disabledButtonBackground};
|
||||
color: ${disabledTextColor};
|
||||
border: 1px solid ${disabledButtonBorder};
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const renderArrow = () => (
|
||||
<StyledArrow>
|
||||
<ChevronRight size={'18px'} />
|
||||
</StyledArrow>
|
||||
);
|
||||
|
||||
export interface ColorButtonProps {
|
||||
loading?: boolean;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
children?: any;
|
||||
selected?: boolean;
|
||||
arrow?: boolean;
|
||||
onClick?: any;
|
||||
withMargin?: string;
|
||||
withBorder?: boolean;
|
||||
fullWidth?: boolean;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
export const ColorButton = ({
|
||||
loading,
|
||||
color,
|
||||
disabled,
|
||||
children,
|
||||
selected,
|
||||
arrow,
|
||||
withMargin,
|
||||
withBorder,
|
||||
fullWidth,
|
||||
width,
|
||||
onClick,
|
||||
}: ColorButtonProps) => {
|
||||
if (disabled && !loading) {
|
||||
return (
|
||||
<DisabledButton
|
||||
withMargin={withMargin}
|
||||
fullWidth={fullWidth}
|
||||
buttonWidth={width}
|
||||
>
|
||||
{children}
|
||||
{arrow && renderArrow()}
|
||||
</DisabledButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DisabledButton
|
||||
withMargin={withMargin}
|
||||
fullWidth={fullWidth}
|
||||
buttonWidth={width}
|
||||
>
|
||||
<ScaleLoader height={16} color={themeColors.blue2} />
|
||||
</DisabledButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BorderButton
|
||||
borderColor={color}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
withMargin={withMargin}
|
||||
withBorder={withBorder}
|
||||
fullWidth={fullWidth}
|
||||
buttonWidth={width}
|
||||
>
|
||||
{children}
|
||||
{arrow && renderArrow()}
|
||||
</BorderButton>
|
||||
);
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { boolean, color } from '@storybook/addon-knobs';
|
||||
import { MultiButton, SingleButton } from './MultiButton';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Multi Button',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const withColor = boolean('With Color', false);
|
||||
|
||||
const buttonColor = withColor ? { color: color('Color', 'yellow') } : {};
|
||||
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
return (
|
||||
<MultiButton>
|
||||
<SingleButton
|
||||
{...buttonColor}
|
||||
selected={selected === 0}
|
||||
onClick={() => {
|
||||
action('Button 1 clicked')();
|
||||
setSelected(0);
|
||||
}}
|
||||
>
|
||||
Button 1
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
{...buttonColor}
|
||||
selected={selected === 1}
|
||||
onClick={() => {
|
||||
action('Button 2 clicked')();
|
||||
setSelected(1);
|
||||
}}
|
||||
>
|
||||
Button 2
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
{...buttonColor}
|
||||
selected={selected === 2}
|
||||
onClick={() => {
|
||||
action('Button 3 clicked')();
|
||||
setSelected(2);
|
||||
}}
|
||||
>
|
||||
Button 3
|
||||
</SingleButton>
|
||||
</MultiButton>
|
||||
);
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
multiSelectColor,
|
||||
colorButtonBorder,
|
||||
multiButtonColor,
|
||||
} from '../../../styles/Themes';
|
||||
|
||||
interface StyledSingleProps {
|
||||
selected?: boolean;
|
||||
buttonColor?: string;
|
||||
}
|
||||
|
||||
const StyledSingleButton = styled.button`
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
background-color: transparent;
|
||||
color: ${multiSelectColor};
|
||||
flex-grow: 1;
|
||||
|
||||
${({ selected, buttonColor }: StyledSingleProps) =>
|
||||
selected
|
||||
? css`
|
||||
color: white;
|
||||
background-color: ${buttonColor
|
||||
? buttonColor
|
||||
: colorButtonBorder};
|
||||
`
|
||||
: ''};
|
||||
`;
|
||||
|
||||
interface SingleButtonProps {
|
||||
children: any;
|
||||
selected?: boolean;
|
||||
color?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const SingleButton = ({
|
||||
children,
|
||||
selected,
|
||||
color,
|
||||
onClick,
|
||||
}: SingleButtonProps) => {
|
||||
return (
|
||||
<StyledSingleButton
|
||||
selected={selected}
|
||||
buttonColor={color}
|
||||
onClick={() => {
|
||||
onClick && onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledSingleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface MultiBackProps {
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
const MultiBackground = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
background: ${multiButtonColor};
|
||||
flex-wrap: wrap;
|
||||
|
||||
${({ margin }: MultiBackProps) => margin && `margin: ${margin}`}
|
||||
`;
|
||||
|
||||
interface MultiButtonProps {
|
||||
children: any;
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
export const MultiButton = ({ children, margin }: MultiButtonProps) => {
|
||||
return <MultiBackground margin={margin}>{children}</MultiBackground>;
|
||||
};
|
|
@ -1,124 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Sub4Title,
|
||||
NoWrapTitle,
|
||||
SubTitle,
|
||||
ResponsiveLine,
|
||||
} from '../../generic/Styled';
|
||||
import { Circle, ChevronRight } from '../../generic/Icons';
|
||||
import styled from 'styled-components';
|
||||
import { useAccount } from '../../../context/AccountContext';
|
||||
import { saveSessionAuth } from '../../../utils/auth';
|
||||
import { useSettings } from '../../../context/SettingsContext';
|
||||
import { textColorMap, mediaDimensions } from '../../../styles/Themes';
|
||||
import { ColorButton } from '../colorButton/ColorButton';
|
||||
import { Input } from '../../input/Input';
|
||||
import { useSize } from 'hooks/UseSize';
|
||||
|
||||
const RadioText = styled.div`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const ButtonRow = styled.div`
|
||||
width: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface LoginProps {
|
||||
macaroon: string;
|
||||
color?: string;
|
||||
callback: any;
|
||||
variables: {};
|
||||
setModalOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const LoginModal = ({
|
||||
macaroon,
|
||||
color,
|
||||
setModalOpen,
|
||||
callback,
|
||||
variables,
|
||||
}: LoginProps) => {
|
||||
const { width } = useSize();
|
||||
const { theme } = useSettings();
|
||||
|
||||
const [pass, setPass] = useState<string>('');
|
||||
const [storeSession, setStoreSession] = useState<boolean>(false);
|
||||
const { host, cert, refreshAccount } = useAccount();
|
||||
|
||||
const handleClick = () => {
|
||||
try {
|
||||
const bytes = CryptoJS.AES.decrypt(macaroon, pass);
|
||||
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (storeSession) {
|
||||
saveSessionAuth(decrypted);
|
||||
refreshAccount();
|
||||
}
|
||||
const auth = { host, macaroon: decrypted, cert };
|
||||
callback({ variables: { ...variables, auth } });
|
||||
setModalOpen(false);
|
||||
} catch (error) {
|
||||
toast.error('Wrong Password');
|
||||
}
|
||||
};
|
||||
|
||||
const renderButton = (
|
||||
onClick: () => void,
|
||||
text: string,
|
||||
selected: boolean,
|
||||
) => (
|
||||
<ColorButton color={color} onClick={onClick}>
|
||||
<Circle
|
||||
size={'10px'}
|
||||
fillcolor={selected ? textColorMap[theme] : ''}
|
||||
/>
|
||||
<RadioText>{text}</RadioText>
|
||||
</ColorButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle>Unlock your Account</SubTitle>
|
||||
<ResponsiveLine>
|
||||
<Sub4Title>Password:</Sub4Title>
|
||||
<Input
|
||||
withMargin={
|
||||
width <= mediaDimensions.mobile ? '0' : '0 0 0 16px'
|
||||
}
|
||||
type={'password'}
|
||||
onChange={(e) => setPass(e.target.value)}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<ResponsiveLine>
|
||||
<NoWrapTitle>Don't ask me again this session:</NoWrapTitle>
|
||||
<ButtonRow>
|
||||
{renderButton(
|
||||
() => setStoreSession(true),
|
||||
'Yes',
|
||||
storeSession,
|
||||
)}
|
||||
{renderButton(
|
||||
() => setStoreSession(false),
|
||||
'No',
|
||||
!storeSession,
|
||||
)}
|
||||
</ButtonRow>
|
||||
</ResponsiveLine>
|
||||
<ColorButton
|
||||
disabled={pass === ''}
|
||||
onClick={handleClick}
|
||||
color={color}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
>
|
||||
Unlock
|
||||
<ChevronRight />
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,63 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import Modal from '../../modal/ReactModal';
|
||||
import { LoginModal } from './LoginModal';
|
||||
import { useAccount } from '../../../context/AccountContext';
|
||||
import { ColorButton } from '../colorButton/ColorButton';
|
||||
import { ColorButtonProps } from '../colorButton/ColorButton';
|
||||
|
||||
interface SecureButtonProps extends ColorButtonProps {
|
||||
callback: any;
|
||||
disabled: boolean;
|
||||
children: any;
|
||||
variables: {};
|
||||
color?: string;
|
||||
withMargin?: string;
|
||||
arrow?: boolean;
|
||||
}
|
||||
|
||||
export const SecureButton = ({
|
||||
callback,
|
||||
color,
|
||||
disabled,
|
||||
children,
|
||||
variables,
|
||||
...props
|
||||
}: SecureButtonProps) => {
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
|
||||
const { host, cert, admin, sessionAdmin } = useAccount();
|
||||
|
||||
if (!admin && !sessionAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const auth = { host, macaroon: sessionAdmin, cert };
|
||||
|
||||
const handleClick = () => setModalOpen(true);
|
||||
|
||||
const onClick = sessionAdmin
|
||||
? () => callback({ variables: { ...variables, auth } })
|
||||
: handleClick;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColorButton
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ColorButton>
|
||||
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
|
||||
<LoginModal
|
||||
color={color}
|
||||
macaroon={admin}
|
||||
setModalOpen={setModalOpen}
|
||||
callback={callback}
|
||||
variables={variables}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
||||
export default {
|
||||
title: 'Checkbox',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [checked, set] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Checkbox checked={checked} onChange={set}>
|
||||
This is a checkbox
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
colorButtonBackground,
|
||||
buttonBorderColor,
|
||||
themeColors,
|
||||
} from '../../styles/Themes';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const FixedWidth = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin: 0px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.div`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin: 0;
|
||||
border: 1px solid ${buttonBorderColor};
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition-duration: 0.3s;
|
||||
background-color: ${colorButtonBackground};
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
|
||||
${({ checked }: { checked: boolean }) =>
|
||||
checked && `background-color: ${themeColors.blue2}`}
|
||||
`;
|
||||
|
||||
type CheckboxProps = {
|
||||
checked: boolean;
|
||||
onChange: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({
|
||||
children,
|
||||
checked,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer onClick={() => onChange(!checked)}>
|
||||
<FixedWidth>
|
||||
<StyledCheckbox checked={checked} />
|
||||
</FixedWidth>
|
||||
{children}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import {
|
||||
useConnectionState,
|
||||
useConnectionDispatch,
|
||||
} from 'context/ConnectionContext';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_CAN_CONNECT } from 'graphql/query';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
|
||||
export const ConnectionCheck = () => {
|
||||
const { connected } = useConnectionState();
|
||||
const dispatch = useConnectionDispatch();
|
||||
|
||||
const { loggedIn, host, viewOnly, cert, sessionAdmin } = useAccount();
|
||||
const auth = {
|
||||
host,
|
||||
macaroon: viewOnly !== '' ? viewOnly : sessionAdmin,
|
||||
cert,
|
||||
};
|
||||
|
||||
const { data, loading } = useQuery(GET_CAN_CONNECT, {
|
||||
variables: { auth },
|
||||
skip: connected || !loggedIn,
|
||||
onError: () => {
|
||||
dispatch({ type: 'error' });
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && data.getNodeInfo) {
|
||||
dispatch({ type: 'connected' });
|
||||
}
|
||||
}, [data, loading, dispatch]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface EmojiProps {
|
||||
symbol: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const Emoji = ({ label, symbol }: EmojiProps) => (
|
||||
<span
|
||||
className="emoji"
|
||||
role="img"
|
||||
aria-label={label ? label : ''}
|
||||
aria-hidden={label ? 'false' : 'true'}
|
||||
>
|
||||
{symbol}
|
||||
</span>
|
||||
);
|
|
@ -1,74 +0,0 @@
|
|||
import React from 'react';
|
||||
import { SmallLink, DarkSubTitle, OverflowText, SingleLine } from './Styled';
|
||||
import { StatusDot, DetailLine } from '../../views/channels/Channels.style';
|
||||
import { formatDistanceStrict, format } from 'date-fns';
|
||||
import { XSvg } from './Icons';
|
||||
|
||||
export const getTransactionLink = (transaction: string) => {
|
||||
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
|
||||
return (
|
||||
<SmallLink href={link} target="_blank">
|
||||
{transaction}
|
||||
</SmallLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const getNodeLink = (publicKey: string) => {
|
||||
const link = `https://1ml.com/node/${publicKey}`;
|
||||
return (
|
||||
<SmallLink href={link} target="_blank">
|
||||
{publicKey}
|
||||
</SmallLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const getDateDif = (date: string) => {
|
||||
return formatDistanceStrict(new Date(date), new Date());
|
||||
};
|
||||
|
||||
export const getFormatDate = (date: string) => {
|
||||
return format(new Date(date), 'dd-MM-yyyy - HH:mm:ss');
|
||||
};
|
||||
|
||||
export const getTooltipType = (theme: string) => {
|
||||
return theme === 'dark' ? 'light' : undefined;
|
||||
};
|
||||
|
||||
export const getStatusDot = (status: boolean, type: string) => {
|
||||
if (type === 'active') {
|
||||
return status ? (
|
||||
<StatusDot color="#95de64" />
|
||||
) : (
|
||||
<StatusDot color="#ff4d4f" />
|
||||
);
|
||||
} else if (type === 'opening') {
|
||||
return status ? <StatusDot color="#13c2c2" /> : null;
|
||||
} else {
|
||||
return status ? <StatusDot color="#ff4d4f" /> : null;
|
||||
}
|
||||
};
|
||||
|
||||
export const renderLine = (
|
||||
title: string,
|
||||
content: any,
|
||||
key?: string | number,
|
||||
deleteCallback?: () => void,
|
||||
) => {
|
||||
if (!content) return null;
|
||||
return (
|
||||
<DetailLine key={key}>
|
||||
<DarkSubTitle>{title}</DarkSubTitle>
|
||||
<SingleLine>
|
||||
<OverflowText>{content}</OverflowText>
|
||||
{deleteCallback && (
|
||||
<div
|
||||
style={{ margin: '0 0 -4px 4px' }}
|
||||
onClick={deleteCallback}
|
||||
>
|
||||
<XSvg />
|
||||
</div>
|
||||
)}
|
||||
</SingleLine>
|
||||
</DetailLine>
|
||||
);
|
||||
};
|
|
@ -1,124 +0,0 @@
|
|||
import { FunctionComponent } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ReactComponent as UpIcon } from '../../assets/icons/arrow-up.svg';
|
||||
import { ReactComponent as DownIcon } from '../../assets/icons/arrow-down.svg';
|
||||
import { ReactComponent as ZapIcon } from '../../assets/icons/zap.svg';
|
||||
import { ReactComponent as ZapOffIcon } from '../../assets/icons/zap-off.svg';
|
||||
import { ReactComponent as HelpIcon } from '../../assets/icons/help-circle.svg';
|
||||
import { ReactComponent as SunIcon } from '../../assets/icons/sun.svg';
|
||||
import { ReactComponent as MoonIcon } from '../../assets/icons/moon.svg';
|
||||
import { ReactComponent as EyeIcon } from '../../assets/icons/eye.svg';
|
||||
import { ReactComponent as EyeOffIcon } from '../../assets/icons/eye-off.svg';
|
||||
import { ReactComponent as ChevronsUpIcon } from '../../assets/icons/chevrons-up.svg';
|
||||
import { ReactComponent as ChevronsDownIcon } from '../../assets/icons/chevrons-down.svg';
|
||||
import { ReactComponent as ChevronLeftIcon } from '../../assets/icons/chevron-left.svg';
|
||||
import { ReactComponent as ChevronRightIcon } from '../../assets/icons/chevron-right.svg';
|
||||
import { ReactComponent as ChevronUpIcon } from '../../assets/icons/chevron-up.svg';
|
||||
import { ReactComponent as ChevronDownIcon } from '../../assets/icons/chevron-down.svg';
|
||||
import { ReactComponent as HomeIcon } from '../../assets/icons/home.svg';
|
||||
import { ReactComponent as CpuIcon } from '../../assets/icons/cpu.svg';
|
||||
import { ReactComponent as SendIcon } from '../../assets/icons/send.svg';
|
||||
import { ReactComponent as ServerIcon } from '../../assets/icons/server.svg';
|
||||
import { ReactComponent as SettingsIcon } from '../../assets/icons/settings.svg';
|
||||
import { ReactComponent as EditIcon } from '../../assets/icons/edit.svg';
|
||||
import { ReactComponent as MoreVerticalIcon } from '../../assets/icons/more-vertical.svg';
|
||||
import { ReactComponent as AnchorIcon } from '../../assets/icons/anchor.svg';
|
||||
import { ReactComponent as PocketIcon } from '../../assets/icons/pocket.svg';
|
||||
import { ReactComponent as GlobeIcon } from '../../assets/icons/globe.svg';
|
||||
import { ReactComponent as XIcon } from '../../assets/icons/x.svg';
|
||||
import { ReactComponent as LayersIcon } from '../../assets/icons/layers.svg';
|
||||
import { ReactComponent as LoaderIcon } from '../../assets/icons/loader.svg';
|
||||
import { ReactComponent as CircleIcon } from '../../assets/icons/circle.svg';
|
||||
import { ReactComponent as AlertTriangleIcon } from '../../assets/icons/alert-triangle.svg';
|
||||
import { ReactComponent as AlertCircleIcon } from '../../assets/icons/alert-circle.svg';
|
||||
import { ReactComponent as GitCommitIcon } from '../../assets/icons/git-commit.svg';
|
||||
import { ReactComponent as GitBranchIcon } from '../../assets/icons/git-branch.svg';
|
||||
import { ReactComponent as RadioIcon } from '../../assets/icons/radio.svg';
|
||||
import { ReactComponent as CopyIcon } from '../../assets/icons/copy.svg';
|
||||
import { ReactComponent as ShieldIcon } from '../../assets/icons/shield.svg';
|
||||
import { ReactComponent as CrosshairIcon } from '../../assets/icons/crosshair.svg';
|
||||
import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
|
||||
import { ReactComponent as SlidersIcon } from '../../assets/icons/sliders.svg';
|
||||
import { ReactComponent as UsersIcon } from '../../assets/icons/users.svg';
|
||||
import { ReactComponent as GitPullRequestIcon } from '../../assets/icons/git-pull-request.svg';
|
||||
import { ReactComponent as Link } from '../../assets/icons/link.svg';
|
||||
import { ReactComponent as Menu } from '../../assets/icons/menu.svg';
|
||||
import { ReactComponent as Mail } from '../../assets/icons/mail.svg';
|
||||
import { ReactComponent as Github } from '../../assets/icons/github.svg';
|
||||
import { ReactComponent as Repeat } from '../../assets/icons/repeat.svg';
|
||||
import { ReactComponent as CheckIcon } from '../../assets/icons/check.svg';
|
||||
import { ReactComponent as StarIcon } from '../../assets/icons/star.svg';
|
||||
import { ReactComponent as HalfStarIcon } from '../../assets/icons/half-star.svg';
|
||||
import { ReactComponent as CreditCardIcon } from '../../assets/icons/credit-card.svg';
|
||||
|
||||
export interface IconProps {
|
||||
color?: string;
|
||||
size?: string;
|
||||
fillcolor?: string;
|
||||
strokeWidth?: string;
|
||||
}
|
||||
|
||||
const GenericStyles = css`
|
||||
height: ${({ size }: IconProps) => (size ? size : '18px')};
|
||||
width: ${({ size }: IconProps) => (size ? size : '18px')};
|
||||
color: ${({ color }: IconProps) => (color ? color : '')};
|
||||
fill: ${({ fillcolor }: IconProps) => (fillcolor ? fillcolor : '')};
|
||||
stroke-width: ${({ strokeWidth }: IconProps) =>
|
||||
strokeWidth ? strokeWidth : '2px'};
|
||||
`;
|
||||
|
||||
const styleIcon = (icon: FunctionComponent) =>
|
||||
styled(icon)`
|
||||
${GenericStyles}
|
||||
`;
|
||||
|
||||
export const QuestionIcon = styleIcon(HelpIcon);
|
||||
export const Zap = styleIcon(ZapIcon);
|
||||
export const ZapOff = styleIcon(ZapOffIcon);
|
||||
export const Anchor = styleIcon(AnchorIcon);
|
||||
export const Pocket = styleIcon(PocketIcon);
|
||||
export const Globe = styleIcon(GlobeIcon);
|
||||
export const UpArrow = styleIcon(UpIcon);
|
||||
export const DownArrow = styleIcon(DownIcon);
|
||||
export const Sun = styleIcon(SunIcon);
|
||||
export const Moon = styleIcon(MoonIcon);
|
||||
export const Eye = styleIcon(EyeIcon);
|
||||
export const EyeOff = styleIcon(EyeOffIcon);
|
||||
export const ChevronsDown = styleIcon(ChevronsDownIcon);
|
||||
export const ChevronsUp = styleIcon(ChevronsUpIcon);
|
||||
export const ChevronLeft = styleIcon(ChevronLeftIcon);
|
||||
export const ChevronRight = styleIcon(ChevronRightIcon);
|
||||
export const ChevronUp = styleIcon(ChevronUpIcon);
|
||||
export const ChevronDown = styleIcon(ChevronDownIcon);
|
||||
export const Home = styleIcon(HomeIcon);
|
||||
export const Cpu = styleIcon(CpuIcon);
|
||||
export const Send = styleIcon(SendIcon);
|
||||
export const Server = styleIcon(ServerIcon);
|
||||
export const Settings = styleIcon(SettingsIcon);
|
||||
export const Edit = styleIcon(EditIcon);
|
||||
export const MoreVertical = styleIcon(MoreVerticalIcon);
|
||||
export const XSvg = styleIcon(XIcon);
|
||||
export const Layers = styleIcon(LayersIcon);
|
||||
export const Loader = styleIcon(LoaderIcon);
|
||||
export const Circle = styleIcon(CircleIcon);
|
||||
export const AlertTriangle = styleIcon(AlertTriangleIcon);
|
||||
export const AlertCircle = styleIcon(AlertCircleIcon);
|
||||
export const GitCommit = styleIcon(GitCommitIcon);
|
||||
export const GitBranch = styleIcon(GitBranchIcon);
|
||||
export const Radio = styleIcon(RadioIcon);
|
||||
export const Copy = styleIcon(CopyIcon);
|
||||
export const Shield = styleIcon(ShieldIcon);
|
||||
export const Crosshair = styleIcon(CrosshairIcon);
|
||||
export const Key = styleIcon(KeyIcon);
|
||||
export const Sliders = styleIcon(SlidersIcon);
|
||||
export const Users = styleIcon(UsersIcon);
|
||||
export const GitPullRequest = styleIcon(GitPullRequestIcon);
|
||||
export const LinkIcon = styleIcon(Link);
|
||||
export const MenuIcon = styleIcon(Menu);
|
||||
export const MailIcon = styleIcon(Mail);
|
||||
export const GithubIcon = styleIcon(Github);
|
||||
export const RepeatIcon = styleIcon(Repeat);
|
||||
export const Check = styleIcon(CheckIcon);
|
||||
export const Star = styleIcon(StarIcon);
|
||||
export const HalfStar = styleIcon(HalfStarIcon);
|
||||
export const CreditCard = styleIcon(CreditCardIcon);
|
|
@ -1,225 +0,0 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
cardColor,
|
||||
cardBorderColor,
|
||||
subCardColor,
|
||||
smallLinkColor,
|
||||
unSelectedNavButton,
|
||||
textColor,
|
||||
buttonBorderColor,
|
||||
chartLinkColor,
|
||||
inverseTextColor,
|
||||
separationColor,
|
||||
mediaWidths,
|
||||
} from '../../styles/Themes';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
|
||||
export const CardWithTitle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CardTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export interface CardProps {
|
||||
bottom?: string;
|
||||
cardPadding?: string;
|
||||
}
|
||||
|
||||
export const Card = styled.div`
|
||||
padding: ${({ cardPadding }: CardProps) => cardPadding ?? '16px'};
|
||||
background: ${cardColor};
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${cardBorderColor};
|
||||
margin-bottom: ${({ bottom }: CardProps) => (bottom ? bottom : '25px')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface SeparationProps {
|
||||
height?: number;
|
||||
lineColor?: string | ThemeSet;
|
||||
}
|
||||
|
||||
export const Separation = styled.div`
|
||||
height: ${({ height }: SeparationProps) => (height ? height : '1')}px;
|
||||
background-color: ${({ lineColor }: SeparationProps) =>
|
||||
lineColor ?? separationColor};
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
interface SubCardProps {
|
||||
color?: string;
|
||||
padding?: string;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
export const SubCard = styled.div`
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
|
||||
padding: ${({ padding }) => (padding ? padding : '16px')};
|
||||
background: ${subCardColor};
|
||||
border: 1px solid ${cardBorderColor};
|
||||
border-left: ${({ color }: SubCardProps) =>
|
||||
color ? `2px solid ${color}` : ''};
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
export const SmallLink = styled.a`
|
||||
text-decoration: none;
|
||||
color: ${smallLinkColor};
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
type SubTitleProps = {
|
||||
subtitleColor?: string | ThemeSet;
|
||||
fontWeight?: string;
|
||||
};
|
||||
|
||||
export const SubTitle = styled.h4`
|
||||
margin: 5px 0;
|
||||
${({ subtitleColor }: SubTitleProps) =>
|
||||
subtitleColor &&
|
||||
css`
|
||||
color: ${subtitleColor};
|
||||
`}
|
||||
font-weight: ${({ fontWeight }: SubTitleProps) =>
|
||||
fontWeight ? fontWeight : '500'};
|
||||
`;
|
||||
|
||||
export const InverseSubtitle = styled(SubTitle)`
|
||||
color: ${inverseTextColor};
|
||||
`;
|
||||
|
||||
export const Sub4Title = styled.h5`
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const NoWrapTitle = styled(Sub4Title)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SingleLine = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const RightAlign = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ColumnLine = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SimpleButton = styled.button`
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
color: ${({ enabled = true }: { enabled?: boolean }) =>
|
||||
enabled ? textColor : unSelectedNavButton};
|
||||
border: 1px solid ${buttonBorderColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SimpleInverseButton = styled(SimpleButton)`
|
||||
color: ${({ enabled = true }: { enabled?: boolean }) =>
|
||||
enabled ? inverseTextColor : unSelectedNavButton};
|
||||
`;
|
||||
|
||||
interface DarkProps {
|
||||
fontSize?: string;
|
||||
bottom?: string;
|
||||
}
|
||||
|
||||
export const DarkSubTitle = styled.div`
|
||||
font-size: ${({ fontSize }: DarkProps) => (fontSize ? fontSize : '14px')};
|
||||
color: ${unSelectedNavButton};
|
||||
margin-bottom: ${({ bottom }: DarkProps) => (bottom ? bottom : '0px')};
|
||||
`;
|
||||
|
||||
interface ColorProps {
|
||||
color: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
export const ColorButton = styled(SimpleButton)`
|
||||
color: ${({ selected }) => (selected ? textColor : chartLinkColor)};
|
||||
border: ${({ selected, color }: ColorProps) =>
|
||||
selected ? `1px solid ${color}` : ''};
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ color }: ColorProps) => color};
|
||||
color: ${textColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const OverflowText = styled.div`
|
||||
margin-left: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveLine = styled(SingleLine)`
|
||||
width: 100%;
|
||||
${({ withWrap }: { withWrap?: boolean }) =>
|
||||
withWrap &&
|
||||
css`
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveCol = styled.div`
|
||||
flex-grow: 1;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveSingle = styled(SingleLine)`
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { select, color, boolean, text } from '@storybook/addon-knobs';
|
||||
import { Input } from './Input';
|
||||
|
||||
export default {
|
||||
title: 'Input',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const withColor = boolean('With Color', false);
|
||||
|
||||
const buttonColor = withColor ? { color: color('Color', 'yellow') } : {};
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...buttonColor}
|
||||
placeholder={text('Placeholder', 'placeholder')}
|
||||
fullWidth={boolean('Full Width', false)}
|
||||
type={select('Type', ['normal', 'number'], 'normal')}
|
||||
onChange={action('change')}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
textColor,
|
||||
colorButtonBorder,
|
||||
inputBackgroundColor,
|
||||
inputBorderColor,
|
||||
} from '../../styles/Themes';
|
||||
|
||||
interface InputProps {
|
||||
color?: string;
|
||||
withMargin?: string;
|
||||
fullWidth?: boolean;
|
||||
inputWidth?: string;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
padding: 5px;
|
||||
height: 30px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid ${inputBorderColor};
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
color: ${textColor};
|
||||
transition: all 0.5s ease;
|
||||
background-color: ${inputBackgroundColor};
|
||||
${({ maxWidth }: InputProps) =>
|
||||
maxWidth &&
|
||||
css`
|
||||
max-width: ${maxWidth};
|
||||
`}
|
||||
width: ${({ fullWidth, inputWidth }: InputProps) =>
|
||||
fullWidth ? '100%' : inputWidth ? inputWidth : 'auto'};
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
|
||||
|
||||
&:hover {
|
||||
border: 1px solid
|
||||
${({ color }: InputProps) => (color ? color : colorButtonBorder)};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 1px solid
|
||||
${({ color }: InputProps) => (color ? color : colorButtonBorder)};
|
||||
}
|
||||
`;
|
||||
|
||||
interface InputCompProps {
|
||||
type?: string;
|
||||
value?: number | string;
|
||||
placeholder?: string;
|
||||
color?: string;
|
||||
withMargin?: string;
|
||||
fullWidth?: boolean;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
onChange: (e: any) => void;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
type,
|
||||
value,
|
||||
placeholder,
|
||||
color,
|
||||
withMargin,
|
||||
fullWidth = true,
|
||||
width,
|
||||
maxWidth,
|
||||
onChange,
|
||||
}: InputCompProps) => {
|
||||
return (
|
||||
<StyledInput
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
color={color}
|
||||
withMargin={withMargin}
|
||||
onChange={(e) => onChange(e)}
|
||||
fullWidth={fullWidth}
|
||||
inputWidth={width}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { Link } from './Link';
|
||||
|
||||
export default {
|
||||
title: 'Link',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const linkText = text('Link Text', 'This is a link');
|
||||
|
||||
return <Link to={'google.com'}>{linkText}</Link>;
|
||||
};
|
|
@ -1,82 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { textColor, linkHighlight } from '../../styles/Themes';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
interface StyledProps {
|
||||
fontColor?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const styledCss = css`
|
||||
color: ${({ fontColor, inheritColor }: StyledProps) =>
|
||||
inheritColor ? 'inherit' : fontColor ?? textColor};
|
||||
text-decoration: none;
|
||||
${({ fullWidth }: StyledProps) =>
|
||||
fullWidth &&
|
||||
css`
|
||||
width: 100%;
|
||||
`};
|
||||
|
||||
:hover {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 0%,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 100%
|
||||
);
|
||||
background-position: 0 100%;
|
||||
background-size: 2px 2px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(
|
||||
({ inheritColor, fontColor, underline, fullWidth, ...rest }) => (
|
||||
<RouterLink {...rest} />
|
||||
),
|
||||
)(() => styledCss);
|
||||
|
||||
const StyledALink = styled.a`
|
||||
${styledCss}
|
||||
`;
|
||||
|
||||
interface LinkProps {
|
||||
children: any;
|
||||
to?: string;
|
||||
href?: string;
|
||||
color?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const Link = ({
|
||||
children,
|
||||
to,
|
||||
href,
|
||||
color,
|
||||
underline,
|
||||
inheritColor,
|
||||
fullWidth,
|
||||
}: LinkProps) => {
|
||||
const props = { fontColor: color, underline, inheritColor, fullWidth };
|
||||
|
||||
if (!to && !href) return null;
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<StyledLink to={to} {...props}>
|
||||
{children}
|
||||
</StyledLink>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StyledALink href={href} {...props}>
|
||||
{children}
|
||||
</StyledALink>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,63 +0,0 @@
|
|||
import React from 'react';
|
||||
import { CardWithTitle, CardTitle, SubTitle, Card } from '../generic/Styled';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import styled from 'styled-components';
|
||||
import { themeColors } from '../../styles/Themes';
|
||||
|
||||
const Loading = styled.div`
|
||||
width: 100%;
|
||||
height: ${({ loadingHeight }: { loadingHeight?: string }) =>
|
||||
loadingHeight ? loadingHeight : 'auto'};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface LoadingCardProps {
|
||||
title?: string;
|
||||
noCard?: boolean;
|
||||
color?: string;
|
||||
noTitle?: boolean;
|
||||
loadingHeight?: string;
|
||||
}
|
||||
|
||||
export const LoadingCard = ({
|
||||
title = '',
|
||||
color,
|
||||
noCard = false,
|
||||
noTitle = false,
|
||||
loadingHeight,
|
||||
}: LoadingCardProps) => {
|
||||
const loadingColor = color ? color : themeColors.blue3;
|
||||
|
||||
if (noCard) {
|
||||
return (
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<ScaleLoader height={20} color={loadingColor} />
|
||||
</Loading>
|
||||
);
|
||||
}
|
||||
|
||||
if (noTitle) {
|
||||
return (
|
||||
<Card>
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<ScaleLoader height={20} color={loadingColor} />
|
||||
</Loading>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<CardTitle>
|
||||
<SubTitle>{title}</SubTitle>
|
||||
</CardTitle>
|
||||
<Card>
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<ScaleLoader height={20} color={loadingColor} />
|
||||
</Loading>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
);
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { progressBackground } from 'styles/Themes';
|
||||
|
||||
const Progress = styled.div`
|
||||
width: 100%;
|
||||
background: ${progressBackground};
|
||||
`;
|
||||
|
||||
interface ProgressBar {
|
||||
percent: number;
|
||||
barColor?: string;
|
||||
}
|
||||
|
||||
const ProgressBar = styled.div`
|
||||
height: 10px;
|
||||
background-color: ${({ barColor }: ProgressBar) =>
|
||||
barColor ? barColor : 'blue'};
|
||||
width: ${({ percent }: ProgressBar) => `${percent}%`};
|
||||
`;
|
||||
|
||||
const getColor = (percent: number) => {
|
||||
switch (true) {
|
||||
case percent < 20:
|
||||
return '#ff4d4f';
|
||||
case percent < 40:
|
||||
return '#ff7a45';
|
||||
case percent < 60:
|
||||
return '#ffa940';
|
||||
case percent < 80:
|
||||
return '#bae637';
|
||||
case percent <= 100:
|
||||
return '#73d13d';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const LoadingBar = ({ percent }: { percent: number }) => (
|
||||
<Progress>
|
||||
<ProgressBar percent={percent} barColor={getColor(percent)} />
|
||||
</Progress>
|
||||
);
|
|
@ -1,61 +0,0 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { cardColor, mediaWidths, themeColors } from '../../styles/Themes';
|
||||
import ReactModal from 'styled-react-modal';
|
||||
|
||||
interface ModalProps {
|
||||
children: ReactNode;
|
||||
isOpen: boolean;
|
||||
noMinWidth?: boolean;
|
||||
closeCallback: () => void;
|
||||
}
|
||||
|
||||
const generalCSS = css`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
background-color: ${cardColor};
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
top: 100%;
|
||||
border-radius: 0px;
|
||||
transform: translateY(-100%) translateX(-50%);
|
||||
width: 100%;
|
||||
min-width: 325px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyleModal = ReactModal.styled`
|
||||
${generalCSS}
|
||||
min-width: 578px;
|
||||
`;
|
||||
|
||||
const StyleModalSmall = ReactModal.styled`
|
||||
${generalCSS}
|
||||
background-color: ${themeColors.white};
|
||||
`;
|
||||
|
||||
const Modal = ({
|
||||
children,
|
||||
isOpen,
|
||||
noMinWidth = false,
|
||||
closeCallback,
|
||||
}: ModalProps) => {
|
||||
const Styled = noMinWidth ? StyleModalSmall : StyleModal;
|
||||
|
||||
return (
|
||||
<Styled
|
||||
isOpen={isOpen}
|
||||
onBackgroundClick={closeCallback}
|
||||
onEscapeKeydown={closeCallback}
|
||||
>
|
||||
{children}
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
|
@ -1,222 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { CLOSE_CHANNEL } from '../../../graphql/mutation';
|
||||
import { useMutation, useQuery } from '@apollo/react-hooks';
|
||||
import {
|
||||
Separation,
|
||||
SingleLine,
|
||||
SubTitle,
|
||||
Sub4Title,
|
||||
} from '../../generic/Styled';
|
||||
import { AlertTriangle } from '../../generic/Icons';
|
||||
import styled from 'styled-components';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
import { GET_BITCOIN_FEES } from '../../../graphql/query';
|
||||
import { SecureButton } from '../../buttons/secureButton/SecureButton';
|
||||
import { ColorButton } from '../../buttons/colorButton/ColorButton';
|
||||
import {
|
||||
MultiButton,
|
||||
SingleButton,
|
||||
} from 'components/buttons/multiButton/MultiButton';
|
||||
import { Input } from 'components/input/Input';
|
||||
|
||||
interface CloseChannelProps {
|
||||
setModalOpen: (status: boolean) => void;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
}
|
||||
|
||||
const WarningCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CenterLine = styled(SingleLine)`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CloseChannel = ({
|
||||
setModalOpen,
|
||||
channelId,
|
||||
channelName,
|
||||
}: CloseChannelProps) => {
|
||||
const [isForce, setIsForce] = useState<boolean>(false);
|
||||
const [isType, setIsType] = useState<string>('none');
|
||||
const [amount, setAmount] = useState<number>(0);
|
||||
const [isConfirmed, setIsConfirmed] = useState<boolean>(false);
|
||||
|
||||
const [fast, setFast] = useState(0);
|
||||
const [halfHour, setHalfHour] = useState(0);
|
||||
const [hour, setHour] = useState(0);
|
||||
|
||||
const { data: feeData } = useQuery(GET_BITCOIN_FEES, {
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (feeData && feeData.getBitcoinFees) {
|
||||
const { fast, halfHour, hour } = feeData.getBitcoinFees;
|
||||
setAmount(fast);
|
||||
setFast(fast);
|
||||
setHalfHour(halfHour);
|
||||
setHour(hour);
|
||||
}
|
||||
}, [feeData]);
|
||||
|
||||
const [closeChannel] = useMutation(CLOSE_CHANNEL, {
|
||||
onCompleted: (data) => {
|
||||
if (data.closeChannel) {
|
||||
toast.success('Channel Closed');
|
||||
}
|
||||
},
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
refetchQueries: [
|
||||
'GetChannels',
|
||||
'GetPendingChannels',
|
||||
'GetClosedChannels',
|
||||
'GetChannelAmountInfo',
|
||||
],
|
||||
});
|
||||
|
||||
const handleOnlyClose = () => setModalOpen(false);
|
||||
|
||||
const renderButton = (
|
||||
onClick: () => void,
|
||||
text: string,
|
||||
selected: boolean,
|
||||
) => (
|
||||
<SingleButton selected={selected} onClick={onClick}>
|
||||
{text}
|
||||
</SingleButton>
|
||||
);
|
||||
|
||||
const renderWarning = () => (
|
||||
<WarningCard>
|
||||
<AlertTriangle size={'32px'} color={'red'} />
|
||||
<SubTitle>Are you sure you want to close the channel?</SubTitle>
|
||||
<SecureButton
|
||||
callback={closeChannel}
|
||||
variables={{
|
||||
id: channelId,
|
||||
forceClose: isForce,
|
||||
...(isType !== 'none'
|
||||
? isType === 'fee'
|
||||
? { tokens: amount }
|
||||
: { target: amount }
|
||||
: {}),
|
||||
}}
|
||||
color={'red'}
|
||||
disabled={false}
|
||||
withMargin={'4px'}
|
||||
>
|
||||
{`Close Channel [ ${channelName}/${channelId} ]`}
|
||||
</SecureButton>
|
||||
<ColorButton withMargin={'4px'} onClick={handleOnlyClose}>
|
||||
Cancel
|
||||
</ColorButton>
|
||||
</WarningCard>
|
||||
);
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<SingleLine>
|
||||
<SubTitle>{`Close Channel`}</SubTitle>
|
||||
<Sub4Title>{`${channelName} [${channelId}]`}</Sub4Title>
|
||||
</SingleLine>
|
||||
<Separation />
|
||||
<SingleLine>
|
||||
<Sub4Title>Fee:</Sub4Title>
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
{renderButton(
|
||||
() => setIsType('none'),
|
||||
'Auto',
|
||||
isType === 'none',
|
||||
)}
|
||||
{renderButton(() => setIsType('fee'), 'Fee', isType === 'fee')}
|
||||
{renderButton(
|
||||
() => setIsType('target'),
|
||||
'Target',
|
||||
isType === 'target',
|
||||
)}
|
||||
</MultiButton>
|
||||
{isType === 'none' && (
|
||||
<>
|
||||
<SingleLine>
|
||||
<Sub4Title>Fee Amount:</Sub4Title>
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
{renderButton(
|
||||
() => setAmount(fast),
|
||||
`Fastest (${fast} sats)`,
|
||||
amount === fast,
|
||||
)}
|
||||
{halfHour !== fast &&
|
||||
renderButton(
|
||||
() => setAmount(halfHour),
|
||||
`Half Hour (${halfHour} sats)`,
|
||||
amount === halfHour,
|
||||
)}
|
||||
{renderButton(
|
||||
() => setAmount(hour),
|
||||
`Hour (${hour} sats)`,
|
||||
amount === hour,
|
||||
)}
|
||||
</MultiButton>
|
||||
</>
|
||||
)}
|
||||
{isType !== 'none' && (
|
||||
<>
|
||||
<SingleLine>
|
||||
<Sub4Title>
|
||||
{isType === 'target'
|
||||
? 'Target Blocks:'
|
||||
: 'Fee (Sats/Byte)'}
|
||||
</Sub4Title>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Input
|
||||
placeholder={
|
||||
isType === 'target' ? 'Blocks' : 'Sats/Byte'
|
||||
}
|
||||
type={'number'}
|
||||
onChange={(e) =>
|
||||
setAmount(parseInt(e.target.value))
|
||||
}
|
||||
/>
|
||||
</SingleLine>
|
||||
</>
|
||||
)}
|
||||
<SingleLine>
|
||||
<Sub4Title>Force Close Channel:</Sub4Title>
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
{renderButton(() => setIsForce(true), `Yes`, isForce)}
|
||||
{renderButton(() => setIsForce(false), `No`, !isForce)}
|
||||
</MultiButton>
|
||||
<Separation />
|
||||
<CenterLine>
|
||||
<ColorButton
|
||||
withMargin={'4px'}
|
||||
withBorder={true}
|
||||
onClick={handleOnlyClose}
|
||||
>
|
||||
Cancel
|
||||
</ColorButton>
|
||||
<ColorButton
|
||||
arrow={true}
|
||||
withMargin={'4px'}
|
||||
withBorder={true}
|
||||
color={'red'}
|
||||
onClick={() => setIsConfirmed(true)}
|
||||
>
|
||||
Close Channel
|
||||
</ColorButton>
|
||||
</CenterLine>
|
||||
</>
|
||||
);
|
||||
|
||||
return isConfirmed ? renderWarning() : renderContent();
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
import React from 'react';
|
||||
import { REMOVE_PEER } from '../../../graphql/mutation';
|
||||
import { useMutation } from '@apollo/react-hooks';
|
||||
import { SubTitle } from '../../generic/Styled';
|
||||
import { AlertTriangle } from '../../generic/Icons';
|
||||
import styled from 'styled-components';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
import { SecureButton } from '../../buttons/secureButton/SecureButton';
|
||||
import { ColorButton } from '../../buttons/colorButton/ColorButton';
|
||||
|
||||
interface RemovePeerProps {
|
||||
setModalOpen: (status: boolean) => void;
|
||||
publicKey: string;
|
||||
peerAlias: string;
|
||||
}
|
||||
|
||||
const WarningCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const RemovePeerModal = ({
|
||||
setModalOpen,
|
||||
publicKey,
|
||||
peerAlias,
|
||||
}: RemovePeerProps) => {
|
||||
const [removePeer, { loading }] = useMutation(REMOVE_PEER, {
|
||||
onCompleted: (data) => {
|
||||
toast.success('Peer Removed');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorContent(error));
|
||||
},
|
||||
refetchQueries: ['GetPeers'],
|
||||
});
|
||||
|
||||
const handleOnlyClose = () => setModalOpen(false);
|
||||
|
||||
return (
|
||||
<WarningCard>
|
||||
<AlertTriangle size={'32px'} color={'red'} />
|
||||
<SubTitle>Are you sure you want to remove this peer?</SubTitle>
|
||||
<SecureButton
|
||||
callback={removePeer}
|
||||
variables={{
|
||||
publicKey: publicKey,
|
||||
}}
|
||||
color={'red'}
|
||||
disabled={loading}
|
||||
withMargin={'4px'}
|
||||
>
|
||||
{`Remove Peer [${peerAlias ?? 'Unknown'}]`}
|
||||
</SecureButton>
|
||||
<ColorButton withMargin={'4px'} onClick={handleOnlyClose}>
|
||||
Cancel
|
||||
</ColorButton>
|
||||
</WarningCard>
|
||||
);
|
||||
};
|
|
@ -1,91 +0,0 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
import { NodeCard } from './NodeCard';
|
||||
import { CardWithTitle, SubTitle } from 'components/generic/Styled';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
StyledNodeBar,
|
||||
NodeBarContainer,
|
||||
} from './NodeInfo.styled';
|
||||
import { QuestionIcon } from 'components/generic/Icons';
|
||||
import styled from 'styled-components';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { useSettings } from 'context/SettingsContext';
|
||||
import { getTooltipType } from 'components/generic/Helpers';
|
||||
|
||||
const StyledQuestion = styled(QuestionIcon)`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export const NodeBar = () => {
|
||||
const { accounts } = useAccount();
|
||||
const { nodeInfo } = useSettings();
|
||||
const slider = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { theme } = useSettings();
|
||||
const tooltipType = getTooltipType(theme);
|
||||
|
||||
const viewOnlyAccounts = accounts.filter(
|
||||
(account) => account.viewOnly !== '',
|
||||
);
|
||||
|
||||
const handleScroll = (decrease?: boolean) => {
|
||||
if (slider.current !== null) {
|
||||
if (decrease) {
|
||||
slider.current.scrollLeft -= 240;
|
||||
} else {
|
||||
slider.current.scrollLeft += 240;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (viewOnlyAccounts.length <= 1 || !nodeInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<SubTitle>
|
||||
Your Nodes
|
||||
<span data-tip data-for="node_info_question">
|
||||
<StyledQuestion size={'14px'} />
|
||||
</span>
|
||||
</SubTitle>
|
||||
<NodeBarContainer>
|
||||
<div
|
||||
onClick={() => {
|
||||
handleScroll(true);
|
||||
}}
|
||||
>
|
||||
<ArrowLeft />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
handleScroll();
|
||||
}}
|
||||
>
|
||||
<ArrowRight />
|
||||
</div>
|
||||
<StyledNodeBar ref={slider}>
|
||||
{viewOnlyAccounts.map((account, index) => (
|
||||
<div key={account.id}>
|
||||
<NodeCard
|
||||
account={account}
|
||||
accountId={account.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</StyledNodeBar>
|
||||
</NodeBarContainer>
|
||||
<ReactTooltip
|
||||
id={'node_info_question'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
Only accounts with a view-only macaroon will appear here.
|
||||
</ReactTooltip>
|
||||
</CardWithTitle>
|
||||
);
|
||||
};
|
|
@ -1,144 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import 'intersection-observer'; // Polyfill
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_NODE_INFO } from 'graphql/query';
|
||||
import {
|
||||
SingleLine,
|
||||
DarkSubTitle,
|
||||
ResponsiveLine,
|
||||
} from 'components/generic/Styled';
|
||||
import { themeColors } from '../../styles/Themes';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { Price } from 'components/price/Price';
|
||||
import Modal from '../modal/ReactModal';
|
||||
import { StatusDot, StatusLine, QuickCard } from './NodeInfo.styled';
|
||||
import { NodeInfoModal } from './NodeInfoModal';
|
||||
|
||||
export const getStatusDot = (status: boolean) => {
|
||||
return status ? (
|
||||
<StatusDot color="#95de64" />
|
||||
) : (
|
||||
<StatusDot color="#ff4d4f" />
|
||||
);
|
||||
};
|
||||
|
||||
interface NodeCardProps {
|
||||
account: any;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export const NodeCard = ({ account, accountId }: NodeCardProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { host, viewOnly, cert } = account;
|
||||
const [ref, inView] = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
const auth = {
|
||||
host,
|
||||
macaroon: viewOnly,
|
||||
cert,
|
||||
};
|
||||
|
||||
const { data, loading, error } = useQuery(GET_NODE_INFO, {
|
||||
variables: { auth },
|
||||
skip: !inView,
|
||||
pollInterval: 10000,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (!inView) {
|
||||
return (
|
||||
<>
|
||||
<StatusLine>{getStatusDot(false)}</StatusLine>
|
||||
<div>-</div>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Lightning</DarkSubTitle>
|
||||
<div>-</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Bitcoin</DarkSubTitle>
|
||||
<div>-</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Channels</DarkSubTitle>
|
||||
<div>-</div>
|
||||
</SingleLine>
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
loading ||
|
||||
!data ||
|
||||
!data.getNodeInfo ||
|
||||
!data.getChannelBalance
|
||||
) {
|
||||
return <ScaleLoader height={20} color={themeColors.blue3} />;
|
||||
} else {
|
||||
const {
|
||||
active_channels_count,
|
||||
closed_channels_count,
|
||||
alias,
|
||||
pending_channels_count,
|
||||
is_synced_to_chain,
|
||||
} = data.getNodeInfo;
|
||||
|
||||
const { confirmedBalance, pendingBalance } = data.getChannelBalance;
|
||||
|
||||
const chainBalance = data.getChainBalance;
|
||||
const pendingChainBalance = data.getPendingChainBalance;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusLine>{getStatusDot(is_synced_to_chain)}</StatusLine>
|
||||
<div>{alias}</div>
|
||||
<ResponsiveLine>
|
||||
<DarkSubTitle>Lightning</DarkSubTitle>
|
||||
<Price amount={confirmedBalance + pendingBalance} />
|
||||
</ResponsiveLine>
|
||||
<ResponsiveLine>
|
||||
<DarkSubTitle>Bitcoin</DarkSubTitle>
|
||||
<Price amount={chainBalance + pendingChainBalance} />
|
||||
</ResponsiveLine>
|
||||
<ResponsiveLine>
|
||||
<DarkSubTitle>Channels</DarkSubTitle>
|
||||
<div>{`${active_channels_count} / ${pending_channels_count} / ${closed_channels_count}`}</div>
|
||||
</ResponsiveLine>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuickCard
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
ref={ref}
|
||||
key={account.id}
|
||||
>
|
||||
{renderContent()}
|
||||
</QuickCard>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeCallback={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<NodeInfoModal
|
||||
account={{
|
||||
...data,
|
||||
}}
|
||||
accountId={accountId}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,105 +0,0 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { Card } from 'components/generic/Styled';
|
||||
import { ChevronLeft, ChevronRight } from 'components/generic/Icons';
|
||||
import {
|
||||
inverseTextColor,
|
||||
buttonBorderColor,
|
||||
textColor,
|
||||
mediaWidths,
|
||||
} from 'styles/Themes';
|
||||
|
||||
const arrowCSS = css`
|
||||
background-color: ${inverseTextColor};
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
display: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid ${buttonBorderColor};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${textColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ArrowLeft = styled(ChevronLeft)`
|
||||
${arrowCSS}
|
||||
transform: translate(-30%, -50%);
|
||||
`;
|
||||
|
||||
export const ArrowRight = styled(ChevronRight)`
|
||||
${arrowCSS}
|
||||
transform: translate(30%, -50%);
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
export const NodeBarContainer = styled.div`
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
&:hover {
|
||||
${ArrowLeft} {
|
||||
display: inline-block;
|
||||
}
|
||||
${ArrowRight} {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledNodeBar = styled.div`
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: none;
|
||||
cursor: pointer;
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const sectionColor = '#69c0ff';
|
||||
|
||||
export const QuickCard = styled(Card)`
|
||||
height: 120px;
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-bottom: 0px;
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
height: unset;
|
||||
width: 160px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${sectionColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StatusLine = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
right: -8px;
|
||||
top: -8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 0 0 -8px 0;
|
||||
`;
|
||||
|
||||
export const StatusDot = styled.div`
|
||||
margin: 0 2px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 100%;
|
||||
background-color: ${({ color }: { color: string }) => color};
|
||||
`;
|
|
@ -1,102 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
SubTitle,
|
||||
SingleLine,
|
||||
DarkSubTitle,
|
||||
Sub4Title,
|
||||
Separation,
|
||||
} from 'components/generic/Styled';
|
||||
import { Price } from 'components/price/Price';
|
||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||
import { useConnectionDispatch } from 'context/ConnectionContext';
|
||||
import { useStatusDispatch } from 'context/StatusContext';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
|
||||
interface NodeInfoModalProps {
|
||||
account: any;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export const NodeInfoModal = ({ account, accountId }: NodeInfoModalProps) => {
|
||||
const dispatch = useConnectionDispatch();
|
||||
const dispatchState = useStatusDispatch();
|
||||
|
||||
const { changeAccount } = useAccount();
|
||||
|
||||
const {
|
||||
active_channels_count,
|
||||
closed_channels_count,
|
||||
alias,
|
||||
pending_channels_count,
|
||||
is_synced_to_chain,
|
||||
peers_count,
|
||||
version,
|
||||
} = account.getNodeInfo;
|
||||
|
||||
const { confirmedBalance, pendingBalance } = account.getChannelBalance;
|
||||
|
||||
const chainBalance = account.getChainBalance;
|
||||
const pendingChainBalance = account.getPendingChainBalance;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle>{alias}</SubTitle>
|
||||
<Separation />
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Version:</DarkSubTitle>
|
||||
<div>{version.split(' ')[0]}</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Is Synced:</DarkSubTitle>
|
||||
<div>{is_synced_to_chain ? 'True' : 'False'}</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Peer Count:</DarkSubTitle>
|
||||
<div>{peers_count}</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Active Channels:</DarkSubTitle>
|
||||
<div>{active_channels_count}</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Pending Channels:</DarkSubTitle>
|
||||
<div>{pending_channels_count}</div>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Closed Channels:</DarkSubTitle>
|
||||
<div>{closed_channels_count}</div>
|
||||
</SingleLine>
|
||||
<Sub4Title>Lightning</Sub4Title>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Balance:</DarkSubTitle>
|
||||
<Price amount={confirmedBalance} />
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Pending:</DarkSubTitle>
|
||||
<Price amount={pendingBalance} />
|
||||
</SingleLine>
|
||||
<Sub4Title>Bitcoin</Sub4Title>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Balance:</DarkSubTitle>
|
||||
<Price amount={chainBalance} />
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<DarkSubTitle>Pending:</DarkSubTitle>
|
||||
<Price amount={pendingChainBalance} />
|
||||
</SingleLine>
|
||||
<ColorButton
|
||||
withMargin={'16px 0 0'}
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'disconnected' });
|
||||
dispatchState({
|
||||
type: 'disconnected',
|
||||
});
|
||||
changeAccount(accountId);
|
||||
}}
|
||||
>
|
||||
Change to this Account
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,86 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useSettings } from 'context/SettingsContext';
|
||||
import { getValue } from 'helpers/Helpers';
|
||||
import { usePriceState } from 'context/PriceContext';
|
||||
|
||||
type PriceProps = {
|
||||
price: number;
|
||||
symbol: string;
|
||||
currency: string;
|
||||
};
|
||||
|
||||
export const Price = ({
|
||||
amount,
|
||||
breakNumber = false,
|
||||
}: {
|
||||
amount: number;
|
||||
breakNumber?: boolean;
|
||||
}) => {
|
||||
const { currency } = useSettings();
|
||||
const { prices, loading, error } = usePriceState();
|
||||
|
||||
let priceProps: PriceProps = {
|
||||
price: 0,
|
||||
symbol: '',
|
||||
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
|
||||
};
|
||||
|
||||
if (prices && !loading && !error) {
|
||||
const current: { last: number; symbol: string } = prices[currency] ?? {
|
||||
last: 0,
|
||||
symbol: '',
|
||||
};
|
||||
|
||||
priceProps = {
|
||||
price: current.last,
|
||||
symbol: current.symbol,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
|
||||
const getFormat = (amount: number) =>
|
||||
getValue({ amount, ...priceProps, breakNumber });
|
||||
|
||||
return <>{getFormat(amount)}</>;
|
||||
};
|
||||
|
||||
export const getPrice = (
|
||||
currency: string,
|
||||
priceContext: {
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
prices?: { [key: string]: { last: number; symbol: string } };
|
||||
},
|
||||
) => ({
|
||||
amount,
|
||||
breakNumber = false,
|
||||
}: {
|
||||
amount: number;
|
||||
breakNumber?: boolean;
|
||||
}) => {
|
||||
const { prices, loading, error } = priceContext;
|
||||
|
||||
let priceProps: PriceProps = {
|
||||
price: 0,
|
||||
symbol: '',
|
||||
currency: currency !== 'btc' && currency !== 'sat' ? 'sat' : currency,
|
||||
};
|
||||
|
||||
if (prices && !loading && !error) {
|
||||
const current: { last: number; symbol: string } = prices[currency] ?? {
|
||||
last: 0,
|
||||
symbol: '',
|
||||
};
|
||||
|
||||
priceProps = {
|
||||
price: current.last,
|
||||
symbol: current.symbol,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
|
||||
const getFormat = (amount: number) =>
|
||||
getValue({ amount, ...priceProps, breakNumber });
|
||||
|
||||
return getFormat(amount);
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { Rating } from './Rating';
|
||||
|
||||
export default {
|
||||
title: 'Ratings',
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const options = {
|
||||
range: true,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
const rating = number('Rating', 5, options);
|
||||
|
||||
return <Rating rating={rating / 10} />;
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Star, HalfStar } from '../../components/generic/Icons';
|
||||
import { themeColors } from '../../styles/Themes';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledStar = styled(Star)`
|
||||
margin-bottom: -1px;
|
||||
`;
|
||||
|
||||
const StyledHalfStar = styled(HalfStar)`
|
||||
margin-bottom: -1px;
|
||||
`;
|
||||
|
||||
const StyledRatings = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
interface RatingProps {
|
||||
rating: number | null;
|
||||
size?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const Rating = ({
|
||||
rating,
|
||||
size = '14px',
|
||||
color = themeColors.blue3,
|
||||
}: RatingProps) => {
|
||||
if (!rating) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const correctRating = Math.min(Math.max(Math.round(rating * 10), 0), 10);
|
||||
|
||||
const amount = (correctRating - (correctRating % 2)) / 2;
|
||||
const hasHalf = correctRating % 2 > 0 ? true : false;
|
||||
|
||||
let stars = [];
|
||||
|
||||
const starConfig = {
|
||||
size,
|
||||
color,
|
||||
};
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i < amount) {
|
||||
stars.push(
|
||||
<StyledStar
|
||||
key={i}
|
||||
{...starConfig}
|
||||
fillcolor={themeColors.blue3}
|
||||
/>,
|
||||
);
|
||||
} else if (hasHalf && i === amount) {
|
||||
stars.push(<StyledHalfStar key={i} {...starConfig} />);
|
||||
} else {
|
||||
stars.push(<StyledStar key={i} {...starConfig} />);
|
||||
}
|
||||
}
|
||||
|
||||
return <StyledRatings>{stars.map((star) => star)}</StyledRatings>;
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
export const ScrollToTop = () => {
|
||||
let history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = history.listen(() => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
return () => {
|
||||
unlisten();
|
||||
};
|
||||
}, [history]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,69 +0,0 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import { backgroundColor, mediaWidths } from 'styles/Themes';
|
||||
|
||||
interface FullWidthProps {
|
||||
padding?: string;
|
||||
withColor?: boolean;
|
||||
sectionColor?: string | ThemeSet;
|
||||
textColor?: string | ThemeSet;
|
||||
}
|
||||
|
||||
const FullWidth = styled.div`
|
||||
width: 100%;
|
||||
${({ padding }: FullWidthProps) =>
|
||||
padding &&
|
||||
css`
|
||||
padding: ${padding};
|
||||
`}
|
||||
${({ textColor }: FullWidthProps) =>
|
||||
textColor &&
|
||||
css`
|
||||
color: ${textColor};
|
||||
`}
|
||||
background-color: ${({ withColor, sectionColor }: FullWidthProps) =>
|
||||
withColor && (sectionColor ? sectionColor : backgroundColor)};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding: 16px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const FixedWidth = styled.div`
|
||||
max-width: 1000px;
|
||||
margin: 0 auto 0 auto;
|
||||
|
||||
@media (max-width: 1035px) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Section = ({
|
||||
fixedWidth = true,
|
||||
withColor = true,
|
||||
children,
|
||||
color,
|
||||
textColor,
|
||||
padding,
|
||||
}: {
|
||||
fixedWidth?: boolean;
|
||||
withColor?: boolean;
|
||||
color?: any;
|
||||
textColor?: any;
|
||||
padding?: string;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const Fixed = fixedWidth ? FixedWidth : React.Fragment;
|
||||
|
||||
return (
|
||||
<FullWidth
|
||||
padding={padding}
|
||||
withColor={withColor}
|
||||
sectionColor={color}
|
||||
textColor={textColor}
|
||||
>
|
||||
<Fixed>{children}</Fixed>
|
||||
</FullWidth>
|
||||
);
|
||||
};
|
|
@ -1,68 +0,0 @@
|
|||
import { useConnectionState } from 'context/ConnectionContext';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_NODE_INFO } from 'graphql/query';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
import { useStatusDispatch } from 'context/StatusContext';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getErrorContent } from 'utils/error';
|
||||
|
||||
export const StatusCheck = () => {
|
||||
const { connected } = useConnectionState();
|
||||
const dispatch = useStatusDispatch();
|
||||
|
||||
const { loggedIn, host, viewOnly, cert, sessionAdmin } = useAccount();
|
||||
const auth = {
|
||||
host,
|
||||
macaroon: viewOnly !== '' ? viewOnly : sessionAdmin,
|
||||
cert,
|
||||
};
|
||||
|
||||
const { data, loading, error, stopPolling } = useQuery(GET_NODE_INFO, {
|
||||
variables: { auth },
|
||||
skip: !connected || !loggedIn,
|
||||
pollInterval: 10000,
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !loggedIn) {
|
||||
stopPolling();
|
||||
}
|
||||
}, [connected, loggedIn, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !loading && !error) {
|
||||
const {
|
||||
getChainBalance,
|
||||
getPendingChainBalance,
|
||||
getChannelBalance,
|
||||
getNodeInfo,
|
||||
} = data;
|
||||
const { alias, is_synced_to_chain, version } = getNodeInfo;
|
||||
const { confirmedBalance, pendingBalance } = getChannelBalance;
|
||||
|
||||
const versionNumber = version.split(' ');
|
||||
const onlyVersion = versionNumber[0].split('-');
|
||||
const numbers = onlyVersion[0].split('.');
|
||||
|
||||
const state = {
|
||||
loading: false,
|
||||
alias,
|
||||
syncedToChain: is_synced_to_chain,
|
||||
version: versionNumber[0],
|
||||
mayorVersion: numbers[0],
|
||||
minorVersion: numbers[1],
|
||||
revision: numbers[2],
|
||||
chainBalance: getChainBalance,
|
||||
chainPending: getPendingChainBalance,
|
||||
channelBalance: confirmedBalance,
|
||||
channelPending: pendingBalance,
|
||||
};
|
||||
|
||||
dispatch({ type: 'connected', state });
|
||||
}
|
||||
}, [data, dispatch, error, loading]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,202 +0,0 @@
|
|||
import React, { createContext, useState, useContext } from 'react';
|
||||
import merge from 'lodash.merge';
|
||||
import { getAuth } from 'utils/auth';
|
||||
import { saveAccounts } from 'utils/storage';
|
||||
|
||||
interface SingleAccountProps {
|
||||
name: string;
|
||||
host: string;
|
||||
admin: string;
|
||||
viewOnly: string;
|
||||
cert: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ChangeProps {
|
||||
loggedIn?: boolean;
|
||||
name?: string;
|
||||
host?: string;
|
||||
admin?: string;
|
||||
sessionAdmin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface AccountProps {
|
||||
loggedIn: boolean;
|
||||
name: string;
|
||||
host: string;
|
||||
admin: string;
|
||||
sessionAdmin: string;
|
||||
viewOnly: string;
|
||||
cert: string;
|
||||
id: string;
|
||||
accounts: SingleAccountProps[];
|
||||
setAccount: (newProps: ChangeProps) => void;
|
||||
changeAccount: (account: string) => void;
|
||||
deleteAccount: (account: string) => void;
|
||||
refreshAccount: () => void;
|
||||
}
|
||||
|
||||
export const AccountContext = createContext<AccountProps>({
|
||||
loggedIn: false,
|
||||
name: '',
|
||||
host: '',
|
||||
admin: '',
|
||||
sessionAdmin: '',
|
||||
viewOnly: '',
|
||||
cert: '',
|
||||
id: '',
|
||||
accounts: [],
|
||||
setAccount: () => {},
|
||||
changeAccount: () => {},
|
||||
deleteAccount: () => {},
|
||||
refreshAccount: () => {},
|
||||
});
|
||||
|
||||
const AccountProvider = ({ children }: any) => {
|
||||
const sessionAdmin = sessionStorage.getItem('session') || '';
|
||||
const {
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
accounts,
|
||||
loggedIn,
|
||||
} = getAuth();
|
||||
|
||||
const setAccount = ({
|
||||
loggedIn,
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
sessionAdmin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
}: ChangeProps) => {
|
||||
updateAccount((prevState: any) => {
|
||||
const newState = { ...prevState };
|
||||
return merge(newState, {
|
||||
loggedIn,
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
sessionAdmin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const changeAccount = (changeToId: string) => {
|
||||
const currentAccounts = JSON.parse(
|
||||
localStorage.getItem('accounts') || '[]',
|
||||
);
|
||||
const index = currentAccounts.findIndex(
|
||||
(account: any) => account.id === changeToId,
|
||||
);
|
||||
|
||||
if (index < 0) return;
|
||||
|
||||
sessionStorage.removeItem('session');
|
||||
localStorage.setItem('active', `${index}`);
|
||||
|
||||
refreshAccount(`${index}`);
|
||||
};
|
||||
|
||||
const deleteAccount = (deleteId: string) => {
|
||||
const currentAccounts = JSON.parse(
|
||||
localStorage.getItem('accounts') || '[]',
|
||||
);
|
||||
const current = currentAccounts.find(
|
||||
(account: any) => account.id === deleteId,
|
||||
);
|
||||
|
||||
if (!current) return;
|
||||
|
||||
const isCurrentAccount = current.id === id;
|
||||
|
||||
const changedAccounts = [...currentAccounts].filter(
|
||||
(account) => account.id !== deleteId,
|
||||
);
|
||||
const length = changedAccounts.length;
|
||||
|
||||
if (isCurrentAccount) {
|
||||
sessionStorage.removeItem('session');
|
||||
localStorage.setItem('active', `${length - 1}`);
|
||||
} else {
|
||||
const newIndex = changedAccounts.findIndex(
|
||||
(account: any) => account.id === id,
|
||||
);
|
||||
localStorage.setItem('active', `${newIndex}`);
|
||||
}
|
||||
|
||||
saveAccounts(changedAccounts);
|
||||
|
||||
refreshAccount();
|
||||
};
|
||||
|
||||
const refreshAccount = (account?: string) => {
|
||||
const sessionAdmin = sessionStorage.getItem('session') || '';
|
||||
const {
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
accounts,
|
||||
loggedIn,
|
||||
} = getAuth(account);
|
||||
|
||||
updateAccount((prevState: any) => {
|
||||
const newState = { ...prevState };
|
||||
|
||||
const merged = merge(newState, {
|
||||
loggedIn,
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
sessionAdmin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
});
|
||||
|
||||
return { ...merged, accounts };
|
||||
});
|
||||
};
|
||||
|
||||
const accountState = {
|
||||
loggedIn,
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
sessionAdmin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
accounts,
|
||||
setAccount,
|
||||
changeAccount,
|
||||
deleteAccount,
|
||||
refreshAccount,
|
||||
};
|
||||
|
||||
const [settings, updateAccount] = useState(accountState);
|
||||
|
||||
return (
|
||||
<AccountContext.Provider value={settings}>
|
||||
{children}
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useAccount = () => useContext(AccountContext);
|
||||
|
||||
export { AccountProvider, useAccount };
|
|
@ -1,76 +0,0 @@
|
|||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
fast: number;
|
||||
halfHour: number;
|
||||
hour: number;
|
||||
};
|
||||
|
||||
type ActionType = {
|
||||
type: 'fetched' | 'error';
|
||||
state?: State;
|
||||
};
|
||||
|
||||
type Dispatch = (action: ActionType) => void;
|
||||
|
||||
export const StateContext = createContext<State | undefined>(undefined);
|
||||
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
const initialState = {
|
||||
loading: true,
|
||||
error: false,
|
||||
fast: 0,
|
||||
halfHour: 0,
|
||||
hour: 0,
|
||||
};
|
||||
|
||||
const stateReducer = (state: State, action: ActionType): State => {
|
||||
switch (action.type) {
|
||||
case 'fetched':
|
||||
return action.state || initialState;
|
||||
case 'error':
|
||||
return {
|
||||
...initialState,
|
||||
loading: false,
|
||||
error: true,
|
||||
};
|
||||
default:
|
||||
return initialState;
|
||||
}
|
||||
};
|
||||
|
||||
const BitcoinInfoProvider = ({ children }: any) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState);
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
</DispatchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useBitcoinState = () => {
|
||||
const context = useContext(StateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useBitcoinState must be used within a BitcoinInfoProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const useBitcoinDispatch = () => {
|
||||
const context = useContext(DispatchContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useBitcoinDispatch must be used within a BitcoinInfoProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { BitcoinInfoProvider, useBitcoinState, useBitcoinDispatch };
|
|
@ -1,68 +0,0 @@
|
|||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
type State = {
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
};
|
||||
|
||||
type ActionType = {
|
||||
type: 'connected' | 'loading' | 'error' | 'disconnected';
|
||||
};
|
||||
|
||||
type Dispatch = (action: ActionType) => void;
|
||||
|
||||
const StateContext = createContext<State | undefined>(undefined);
|
||||
const DispatchContext = createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
const stateReducer = (state: State, action: ActionType) => {
|
||||
switch (action.type) {
|
||||
case 'connected':
|
||||
return { connected: true, loading: false, error: false };
|
||||
case 'loading':
|
||||
case 'disconnected':
|
||||
return { connected: false, loading: true, error: false };
|
||||
default:
|
||||
return { connected: false, loading: false, error: true };
|
||||
}
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
connected: false,
|
||||
loading: true,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const ConnectionProvider = ({ children }: any) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState);
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useConnectionState = () => {
|
||||
const context = useContext(StateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useConnectionState must be used within a ConnectionProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const useConnectionDispatch = () => {
|
||||
const context = useContext(DispatchContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useConnectionDispatch must be used within a ConnectionProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { ConnectionProvider, useConnectionState, useConnectionDispatch };
|
|
@ -1,73 +0,0 @@
|
|||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
type PriceProps = {
|
||||
last: number;
|
||||
symbol: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
prices?: { [key: string]: PriceProps };
|
||||
};
|
||||
|
||||
type ActionType = {
|
||||
type: 'fetched' | 'error';
|
||||
state?: State;
|
||||
};
|
||||
|
||||
type Dispatch = (action: ActionType) => void;
|
||||
|
||||
export const StateContext = createContext<State | undefined>(undefined);
|
||||
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
const initialState: State = {
|
||||
loading: true,
|
||||
error: false,
|
||||
prices: { EUR: { last: 0, symbol: '€' } },
|
||||
};
|
||||
|
||||
const stateReducer = (state: State, action: ActionType): State => {
|
||||
switch (action.type) {
|
||||
case 'fetched':
|
||||
return action.state || initialState;
|
||||
case 'error':
|
||||
return {
|
||||
...initialState,
|
||||
loading: false,
|
||||
error: true,
|
||||
};
|
||||
default:
|
||||
return initialState;
|
||||
}
|
||||
};
|
||||
|
||||
const PriceProvider = ({ children }: any) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState);
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
</DispatchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const usePriceState = () => {
|
||||
const context = useContext(StateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePriceState must be used within a PriceProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const usePriceDispatch = () => {
|
||||
const context = useContext(DispatchContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePriceDispatch must be used within a PriceProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { PriceProvider, usePriceState, usePriceDispatch };
|
|
@ -1,90 +0,0 @@
|
|||
import React, { createContext, useState, useContext } from 'react';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
interface ChangeProps {
|
||||
theme?: string;
|
||||
sidebar?: boolean;
|
||||
currency?: string;
|
||||
nodeInfo?: boolean;
|
||||
}
|
||||
|
||||
interface SettingsProps {
|
||||
currency: string;
|
||||
theme: string;
|
||||
sidebar: boolean;
|
||||
nodeInfo: boolean;
|
||||
setSettings: (newProps: ChangeProps) => void;
|
||||
refreshSettings: () => void;
|
||||
}
|
||||
|
||||
export const SettingsContext = createContext<SettingsProps>({
|
||||
currency: '',
|
||||
theme: '',
|
||||
sidebar: true,
|
||||
nodeInfo: false,
|
||||
setSettings: () => {},
|
||||
refreshSettings: () => {},
|
||||
});
|
||||
|
||||
const SettingsProvider = ({ children }: any) => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
const savedSidebar =
|
||||
localStorage.getItem('sidebar') === 'false' ? false : true;
|
||||
const savedCurrency = localStorage.getItem('currency') || 'sat';
|
||||
const savedNodeInfo =
|
||||
localStorage.getItem('nodeInfo') === 'true' ? true : false;
|
||||
|
||||
const refreshSettings = (account?: string) => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
const savedSidebar =
|
||||
localStorage.getItem('sidebar') === 'false' ? false : true;
|
||||
const savedCurrency = localStorage.getItem('currency') || 'sat';
|
||||
const savedNodeInfo =
|
||||
localStorage.getItem('nodeInfo') === 'true' ? true : false;
|
||||
|
||||
updateSettings((prevState: any) => {
|
||||
const newState = { ...prevState };
|
||||
return merge(newState, {
|
||||
currency: savedCurrency,
|
||||
theme: savedTheme,
|
||||
sidebar: savedSidebar,
|
||||
nodeInfo: savedNodeInfo,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setSettings = ({ currency, theme, sidebar }: ChangeProps) => {
|
||||
updateSettings((prevState: any) => {
|
||||
const newState = { ...prevState };
|
||||
return merge(newState, {
|
||||
currency,
|
||||
theme,
|
||||
sidebar,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const settingsState = {
|
||||
prices: { EUR: { last: 0, symbol: '€' } },
|
||||
price: 0,
|
||||
symbol: '',
|
||||
currency: savedCurrency,
|
||||
theme: savedTheme,
|
||||
sidebar: savedSidebar,
|
||||
nodeInfo: savedNodeInfo,
|
||||
setSettings,
|
||||
refreshSettings,
|
||||
};
|
||||
|
||||
const [settings, updateSettings] = useState(settingsState);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useSettings = () => useContext(SettingsContext);
|
||||
|
||||
export { SettingsProvider, useSettings };
|
|
@ -1,82 +0,0 @@
|
|||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
alias: string;
|
||||
syncedToChain: boolean;
|
||||
version: string;
|
||||
mayorVersion: number;
|
||||
minorVersion: number;
|
||||
revision: number;
|
||||
chainBalance: number;
|
||||
chainPending: number;
|
||||
channelBalance: number;
|
||||
channelPending: number;
|
||||
};
|
||||
|
||||
type ActionType = {
|
||||
type: 'connected' | 'disconnected';
|
||||
state?: State;
|
||||
};
|
||||
|
||||
type Dispatch = (action: ActionType) => void;
|
||||
|
||||
const StateContext = createContext<State | undefined>(undefined);
|
||||
const DispatchContext = createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
const stateReducer = (state: State, action: ActionType): State => {
|
||||
switch (action.type) {
|
||||
case 'connected':
|
||||
return action.state || initialState;
|
||||
case 'disconnected':
|
||||
return initialState;
|
||||
default:
|
||||
return initialState;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
loading: true,
|
||||
alias: '',
|
||||
syncedToChain: false,
|
||||
version: '',
|
||||
mayorVersion: 0,
|
||||
minorVersion: 0,
|
||||
revision: 0,
|
||||
chainBalance: 0,
|
||||
chainPending: 0,
|
||||
channelBalance: 0,
|
||||
channelPending: 0,
|
||||
};
|
||||
|
||||
const StatusProvider = ({ children }: any) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState);
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
</DispatchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useStatusState = () => {
|
||||
const context = useContext(StateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useStatusState must be used within a StatusProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const useStatusDispatch = () => {
|
||||
const context = useContext(DispatchContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
'useStatusDispatch must be used within a StatusProvider',
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { StatusProvider, useStatusState, useStatusDispatch };
|
|
@ -1,71 +0,0 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_HODL_COUNTRIES = gql`
|
||||
query GetCountries {
|
||||
getCountries {
|
||||
code
|
||||
name
|
||||
native_name
|
||||
currency_code
|
||||
currency_name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_HODL_CURRENCIES = gql`
|
||||
query GetCurrencies {
|
||||
getCurrencies {
|
||||
code
|
||||
name
|
||||
type
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_HODL_OFFERS = gql`
|
||||
query GetOffers($filter: String) {
|
||||
getOffers(filter: $filter) {
|
||||
id
|
||||
asset_code
|
||||
country
|
||||
country_code
|
||||
working_now
|
||||
side
|
||||
title
|
||||
description
|
||||
currency_code
|
||||
price
|
||||
min_amount
|
||||
max_amount
|
||||
first_trade_limit
|
||||
fee {
|
||||
author_fee_rate
|
||||
}
|
||||
balance
|
||||
payment_window_minutes
|
||||
confirmations
|
||||
payment_method_instructions {
|
||||
id
|
||||
version
|
||||
payment_method_id
|
||||
payment_method_type
|
||||
payment_method_name
|
||||
}
|
||||
trader {
|
||||
login
|
||||
online_status
|
||||
rating
|
||||
trades_count
|
||||
url
|
||||
verified
|
||||
verified_by
|
||||
strong_hodler
|
||||
country
|
||||
country_code
|
||||
average_payment_time_minutes
|
||||
average_release_time_minutes
|
||||
days_since_last_trade
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -1,159 +0,0 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const CLOSE_CHANNEL = gql`
|
||||
mutation CloseChannel(
|
||||
$id: String!
|
||||
$auth: authType!
|
||||
$forceClose: Boolean
|
||||
$target: Int
|
||||
$tokens: Int
|
||||
) {
|
||||
closeChannel(
|
||||
id: $id
|
||||
forceClose: $forceClose
|
||||
targetConfirmations: $target
|
||||
tokensPerVByte: $tokens
|
||||
auth: $auth
|
||||
) {
|
||||
transactionId
|
||||
transactionOutputIndex
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const OPEN_CHANNEL = gql`
|
||||
mutation openChannel(
|
||||
$amount: Int!
|
||||
$partnerPublicKey: String!
|
||||
$auth: authType!
|
||||
$tokensPerVByte: Int
|
||||
$isPrivate: Boolean
|
||||
) {
|
||||
openChannel(
|
||||
amount: $amount
|
||||
partnerPublicKey: $partnerPublicKey
|
||||
auth: $auth
|
||||
tokensPerVByte: $tokensPerVByte
|
||||
isPrivate: $isPrivate
|
||||
) {
|
||||
transactionId
|
||||
transactionOutputIndex
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PAY_INVOICE = gql`
|
||||
mutation PayInvoice($request: String!, $auth: authType!) {
|
||||
pay(request: $request, auth: $auth) {
|
||||
isConfirmed
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CREATE_INVOICE = gql`
|
||||
mutation PayInvoice($amount: Int!, $auth: authType!) {
|
||||
createInvoice(amount: $amount, auth: $auth) {
|
||||
request
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CREATE_ADDRESS = gql`
|
||||
mutation CreateAddress($nested: Boolean, $auth: authType!) {
|
||||
createAddress(nested: $nested, auth: $auth)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PAY_ADDRESS = gql`
|
||||
mutation PayAddress(
|
||||
$auth: authType!
|
||||
$address: String!
|
||||
$tokens: Int
|
||||
$fee: Int
|
||||
$target: Int
|
||||
$sendAll: Boolean
|
||||
) {
|
||||
sendToAddress(
|
||||
auth: $auth
|
||||
address: $address
|
||||
tokens: $tokens
|
||||
fee: $fee
|
||||
target: $target
|
||||
sendAll: $sendAll
|
||||
) {
|
||||
confirmationCount
|
||||
id
|
||||
isConfirmed
|
||||
isOutgoing
|
||||
tokens
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DECODE_REQUEST = gql`
|
||||
mutation decodeRequest($auth: authType!, $request: String!) {
|
||||
decodeRequest(auth: $auth, request: $request) {
|
||||
chainAddress
|
||||
cltvDelta
|
||||
description
|
||||
descriptionHash
|
||||
destination
|
||||
expiresAt
|
||||
id
|
||||
routes {
|
||||
baseFeeMTokens
|
||||
channel
|
||||
cltvDelta
|
||||
feeRate
|
||||
publicKey
|
||||
}
|
||||
tokens
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_FEES = gql`
|
||||
mutation updateFees(
|
||||
$auth: authType!
|
||||
$transactionId: String
|
||||
$transactionVout: Int
|
||||
$baseFee: Int
|
||||
$feeRate: Int
|
||||
) {
|
||||
updateFees(
|
||||
auth: $auth
|
||||
transactionId: $transactionId
|
||||
transactionVout: $transactionVout
|
||||
baseFee: $baseFee
|
||||
feeRate: $feeRate
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const PAY_VIA_ROUTE = gql`
|
||||
mutation PayViaRoute($auth: authType!, $route: String!) {
|
||||
payViaRoute(auth: $auth, route: $route)
|
||||
}
|
||||
`;
|
||||
|
||||
export const REMOVE_PEER = gql`
|
||||
mutation RemovePeer($auth: authType!, $publicKey: String!) {
|
||||
removePeer(auth: $auth, publicKey: $publicKey)
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_PEER = gql`
|
||||
mutation AddPeer(
|
||||
$auth: authType!
|
||||
$publicKey: String!
|
||||
$socket: String!
|
||||
$isTemporary: Boolean
|
||||
) {
|
||||
addPeer(
|
||||
auth: $auth
|
||||
publicKey: $publicKey
|
||||
socket: $socket
|
||||
isTemporary: $isTemporary
|
||||
)
|
||||
}
|
||||
`;
|
|
@ -1,378 +0,0 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_NETWORK_INFO = gql`
|
||||
query GetNetworkInfo($auth: authType!) {
|
||||
getNetworkInfo(auth: $auth) {
|
||||
averageChannelSize
|
||||
channelCount
|
||||
maxChannelSize
|
||||
medianChannelSize
|
||||
minChannelSize
|
||||
nodeCount
|
||||
notRecentlyUpdatedPolicyCount
|
||||
totalCapacity
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CAN_CONNECT = gql`
|
||||
query GetNodeInfo($auth: authType!) {
|
||||
getNodeInfo(auth: $auth) {
|
||||
chains
|
||||
color
|
||||
active_channels_count
|
||||
closed_channels_count
|
||||
alias
|
||||
is_synced_to_chain
|
||||
peers_count
|
||||
pending_channels_count
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CAN_ADMIN = gql`
|
||||
query AdminCheck($auth: authType!) {
|
||||
adminCheck(auth: $auth)
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_NODE_INFO = gql`
|
||||
query GetNodeInfo($auth: authType!) {
|
||||
getNodeInfo(auth: $auth) {
|
||||
chains
|
||||
color
|
||||
active_channels_count
|
||||
closed_channels_count
|
||||
alias
|
||||
is_synced_to_chain
|
||||
peers_count
|
||||
pending_channels_count
|
||||
version
|
||||
}
|
||||
getChainBalance(auth: $auth)
|
||||
getPendingChainBalance(auth: $auth)
|
||||
getChannelBalance(auth: $auth) {
|
||||
confirmedBalance
|
||||
pendingBalance
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CHANNEL_AMOUNT_INFO = gql`
|
||||
query GetChannelAmountInfo($auth: authType!) {
|
||||
getNodeInfo(auth: $auth) {
|
||||
active_channels_count
|
||||
closed_channels_count
|
||||
pending_channels_count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CHANNELS = gql`
|
||||
query GetChannels($auth: authType!, $active: Boolean) {
|
||||
getChannels(auth: $auth, active: $active) {
|
||||
capacity
|
||||
commit_transaction_fee
|
||||
commit_transaction_weight
|
||||
id
|
||||
is_active
|
||||
is_closing
|
||||
is_opening
|
||||
is_partner_initiated
|
||||
is_private
|
||||
is_static_remote_key
|
||||
local_balance
|
||||
local_reserve
|
||||
partner_public_key
|
||||
received
|
||||
remote_balance
|
||||
remote_reserve
|
||||
sent
|
||||
time_offline
|
||||
time_online
|
||||
transaction_id
|
||||
transaction_vout
|
||||
unsettled_balance
|
||||
partner_node_info {
|
||||
alias
|
||||
capacity
|
||||
channel_count
|
||||
color
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PENDING_CHANNELS = gql`
|
||||
query GetPendingChannels($auth: authType!) {
|
||||
getPendingChannels(auth: $auth) {
|
||||
close_transaction_id
|
||||
is_active
|
||||
is_closing
|
||||
is_opening
|
||||
local_balance
|
||||
local_reserve
|
||||
partner_public_key
|
||||
received
|
||||
remote_balance
|
||||
remote_reserve
|
||||
sent
|
||||
transaction_fee
|
||||
transaction_id
|
||||
transaction_vout
|
||||
partner_node_info {
|
||||
alias
|
||||
capacity
|
||||
channel_count
|
||||
color
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CLOSED_CHANNELS = gql`
|
||||
query GetClosedChannels($auth: authType!) {
|
||||
getClosedChannels(auth: $auth) {
|
||||
capacity
|
||||
close_confirm_height
|
||||
close_transaction_id
|
||||
final_local_balance
|
||||
final_time_locked_balance
|
||||
id
|
||||
is_breach_close
|
||||
is_cooperative_close
|
||||
is_funding_cancel
|
||||
is_local_force_close
|
||||
is_remote_force_close
|
||||
partner_public_key
|
||||
transaction_id
|
||||
transaction_vout
|
||||
partner_node_info {
|
||||
alias
|
||||
capacity
|
||||
channel_count
|
||||
color
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_RESUME = gql`
|
||||
query GetResume($auth: authType!, $token: String) {
|
||||
getResume(auth: $auth, token: $token) {
|
||||
token
|
||||
resume
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_BITCOIN_PRICE = gql`
|
||||
query GetBitcoinPrice {
|
||||
getBitcoinPrice
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_BITCOIN_FEES = gql`
|
||||
query GetBitcoinFees {
|
||||
getBitcoinFees {
|
||||
fast
|
||||
halfHour
|
||||
hour
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_FORWARD_REPORT = gql`
|
||||
query GetForwardReport($time: String, $auth: authType!) {
|
||||
getForwardReport(time: $time, auth: $auth)
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_LIQUID_REPORT = gql`
|
||||
query GetLiquidReport($auth: authType!) {
|
||||
getChannelReport(auth: $auth) {
|
||||
local
|
||||
remote
|
||||
maxIn
|
||||
maxOut
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_FORWARD_CHANNELS_REPORT = gql`
|
||||
query GetForwardChannelsReport(
|
||||
$time: String
|
||||
$order: String
|
||||
$type: String
|
||||
$auth: authType!
|
||||
) {
|
||||
getForwardChannelsReport(
|
||||
time: $time
|
||||
order: $order
|
||||
auth: $auth
|
||||
type: $type
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_IN_OUT = gql`
|
||||
query GetInOut($auth: authType!, $time: String) {
|
||||
getInOut(auth: $auth, time: $time) {
|
||||
invoices
|
||||
payments
|
||||
confirmedInvoices
|
||||
unConfirmedInvoices
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CHAIN_TRANSACTIONS = gql`
|
||||
query GetChainTransactions($auth: authType!) {
|
||||
getChainTransactions(auth: $auth) {
|
||||
block_id
|
||||
confirmation_count
|
||||
confirmation_height
|
||||
created_at
|
||||
fee
|
||||
id
|
||||
output_addresses
|
||||
tokens
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_FORWARDS = gql`
|
||||
query GetForwards($auth: authType!, $time: String) {
|
||||
getForwards(auth: $auth, time: $time) {
|
||||
forwards {
|
||||
created_at
|
||||
fee
|
||||
fee_mtokens
|
||||
incoming_channel
|
||||
incoming_alias
|
||||
incoming_color
|
||||
mtokens
|
||||
outgoing_channel
|
||||
outgoing_alias
|
||||
outgoing_color
|
||||
tokens
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CONNECT_INFO = gql`
|
||||
query GetNodeInfo($auth: authType!) {
|
||||
getNodeInfo(auth: $auth) {
|
||||
public_key
|
||||
uris
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_BACKUPS = gql`
|
||||
query GetBackups($auth: authType!) {
|
||||
getBackups(auth: $auth)
|
||||
}
|
||||
`;
|
||||
|
||||
export const VERIFY_BACKUPS = gql`
|
||||
query VerifyBackups($auth: authType!, $backup: String!) {
|
||||
verifyBackups(auth: $auth, backup: $backup)
|
||||
}
|
||||
`;
|
||||
|
||||
export const SIGN_MESSAGE = gql`
|
||||
query SignMessage($auth: authType!, $message: String!) {
|
||||
signMessage(auth: $auth, message: $message)
|
||||
}
|
||||
`;
|
||||
|
||||
export const VERIFY_MESSAGE = gql`
|
||||
query VerifyMessage(
|
||||
$auth: authType!
|
||||
$message: String!
|
||||
$signature: String!
|
||||
) {
|
||||
verifyMessage(auth: $auth, message: $message, signature: $signature)
|
||||
}
|
||||
`;
|
||||
|
||||
export const RECOVER_FUNDS = gql`
|
||||
query RecoverFunds($auth: authType!, $backup: String!) {
|
||||
recoverFunds(auth: $auth, backup: $backup)
|
||||
}
|
||||
`;
|
||||
|
||||
export const CHANNEL_FEES = gql`
|
||||
query GetChannelFees($auth: authType!) {
|
||||
getChannelFees(auth: $auth) {
|
||||
alias
|
||||
color
|
||||
baseFee
|
||||
feeRate
|
||||
transactionId
|
||||
transactionVout
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_ROUTES = gql`
|
||||
query GetRoutes(
|
||||
$auth: authType!
|
||||
$outgoing: String!
|
||||
$incoming: String!
|
||||
$tokens: Int!
|
||||
$maxFee: Int
|
||||
) {
|
||||
getRoutes(
|
||||
auth: $auth
|
||||
outgoing: $outgoing
|
||||
incoming: $incoming
|
||||
tokens: $tokens
|
||||
maxFee: $maxFee
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PEERS = gql`
|
||||
query GetPeers($auth: authType!) {
|
||||
getPeers(auth: $auth) {
|
||||
bytes_received
|
||||
bytes_sent
|
||||
is_inbound
|
||||
is_sync_peer
|
||||
ping_time
|
||||
public_key
|
||||
socket
|
||||
tokens_received
|
||||
tokens_sent
|
||||
partner_node_info {
|
||||
alias
|
||||
capacity
|
||||
channel_count
|
||||
color
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_UTXOS = gql`
|
||||
query GetUtxos($auth: authType!) {
|
||||
getUtxos(auth: $auth) {
|
||||
address
|
||||
address_format
|
||||
confirmation_count
|
||||
output_script
|
||||
tokens
|
||||
transaction_id
|
||||
transaction_vout
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -1,76 +0,0 @@
|
|||
import numeral from 'numeral';
|
||||
|
||||
const getValueString = (amount: number): string => {
|
||||
if (amount >= 100000) {
|
||||
return `${amount / 1000000}m`;
|
||||
} else if (amount >= 1000) {
|
||||
return `${amount / 1000}k`;
|
||||
}
|
||||
return `${amount}`;
|
||||
};
|
||||
|
||||
interface GetNumberProps {
|
||||
amount: string | number;
|
||||
price: number;
|
||||
symbol: string;
|
||||
currency: string;
|
||||
breakNumber?: boolean;
|
||||
}
|
||||
|
||||
export const getValue = ({
|
||||
amount,
|
||||
price,
|
||||
symbol,
|
||||
currency,
|
||||
breakNumber,
|
||||
}: GetNumberProps): string => {
|
||||
let value: number = 0;
|
||||
if (typeof amount === 'string') {
|
||||
value = parseInt(amount);
|
||||
} else {
|
||||
value = amount;
|
||||
}
|
||||
|
||||
if (currency === 'btc') {
|
||||
if (!value) return `₿0.0`;
|
||||
const amountInBtc = value / 100000000;
|
||||
return `₿${amountInBtc}`;
|
||||
} else if (currency === 'sat') {
|
||||
const breakAmount = breakNumber
|
||||
? getValueString(value)
|
||||
: numeral(value).format('0,0');
|
||||
return `${breakAmount} sats`;
|
||||
} else {
|
||||
const amountInFiat = (value / 100000000) * price;
|
||||
return `${symbol}${numeral(amountInFiat).format('0,0.00')}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPercent = (
|
||||
local: number,
|
||||
remote: number,
|
||||
withDecimals?: boolean,
|
||||
): number => {
|
||||
const total = remote + local;
|
||||
const percent = (local / total) * 100;
|
||||
|
||||
if (remote === 0 && local === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (withDecimals) {
|
||||
return Math.round(percent * 100) / 100;
|
||||
}
|
||||
|
||||
return Math.round(percent);
|
||||
};
|
||||
|
||||
export const saveToPc = (jsonData: string, filename: string) => {
|
||||
const fileData = jsonData;
|
||||
const blob = new Blob([fileData], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.download = `${filename}.txt`;
|
||||
link.href = url;
|
||||
link.click();
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useInterval = (callback: any, delay: number) => {
|
||||
const savedCallback = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
savedCallback.current();
|
||||
};
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}, [delay]);
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
const getSize = () => {
|
||||
const isClient = typeof window === 'object';
|
||||
return {
|
||||
width: isClient ? window.innerWidth : 0,
|
||||
height: isClient ? window.innerHeight : 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSize = () => {
|
||||
const [windowSize, setWindowSize] = useState(getSize());
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize(getSize());
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
const debouncedHandle = debounce(handleResize, 250);
|
||||
window.addEventListener('resize', debouncedHandle);
|
||||
return () => window.removeEventListener('resize', debouncedHandle);
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import './styles/FontStyles.css';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
1
client/src/react-app-env.d.ts
vendored
1
client/src/react-app-env.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -1,104 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Navigation } from '../../sections/navigation/Navigation';
|
||||
import { Switch, Route } from 'react-router';
|
||||
|
||||
import { Home } from '../../views/home/Home';
|
||||
import { NotFound } from '../../views/notFound/NotFound';
|
||||
import { ChannelView } from '../../views/channels/ChannelView';
|
||||
import { SettingsView } from '../../views/settings/Settings';
|
||||
import { TransactionList } from '../../views/transactions/TransactionList';
|
||||
import { FeesView } from '../../views/fees/Fees';
|
||||
import { ForwardsList } from '../../views/forwards/ForwardList';
|
||||
import { TermsView } from '../../views/other/terms/TermsView';
|
||||
import { PrivacyView } from '../../views/other/privacy/PrivacyView';
|
||||
import { FaqView } from '../../views/other/faq/FaqView';
|
||||
import { Section } from 'components/section/Section';
|
||||
import { BitcoinPrice } from '../../components/bitcoinInfo/BitcoinPrice';
|
||||
import { BitcoinFees } from '../../components/bitcoinInfo/BitcoinFees';
|
||||
import { mediaWidths } from 'styles/Themes';
|
||||
import { useConnectionState } from 'context/ConnectionContext';
|
||||
import { LoadingView, ErrorView } from 'views/stateViews/StateCards';
|
||||
import { BalanceView } from 'views/balance/Balance';
|
||||
import { PeersList } from 'views/peers/PeersList';
|
||||
import { ToolsView } from 'views/tools';
|
||||
import { ChainView } from 'views/chain/ChainView';
|
||||
import { TraderView } from 'views/trader/TraderView';
|
||||
|
||||
const Container = styled.div`
|
||||
display: grid;
|
||||
grid-template-areas: 'nav content content';
|
||||
grid-template-columns: auto 1fr 200px;
|
||||
gap: 16px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentStyle = styled.div`
|
||||
grid-area: content;
|
||||
`;
|
||||
|
||||
const Content = () => {
|
||||
const { loading, error } = useConnectionState();
|
||||
|
||||
const renderSettings = (type: string) => (
|
||||
<Switch>
|
||||
<Route path="/settings" render={() => getGrid(SettingsView)} />
|
||||
<Route
|
||||
path="*"
|
||||
render={() =>
|
||||
getGrid(type === 'error' ? ErrorView : LoadingView)
|
||||
}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
if (loading) return renderSettings('loading');
|
||||
if (error) return renderSettings('error');
|
||||
|
||||
return (
|
||||
<>
|
||||
<BitcoinPrice />
|
||||
<BitcoinFees />
|
||||
<Switch>
|
||||
<Route exact path="/" render={() => getGrid(Home)} />
|
||||
<Route path="/peers" render={() => getGrid(PeersList)} />
|
||||
<Route path="/channels" render={() => getGrid(ChannelView)} />
|
||||
<Route path="/balance" render={() => getGrid(BalanceView)} />
|
||||
<Route path="/tools" render={() => getGrid(ToolsView)} />
|
||||
<Route
|
||||
path="/transactions"
|
||||
render={() => getGrid(TransactionList)}
|
||||
/>
|
||||
<Route path="/forwards" render={() => getGrid(ForwardsList)} />
|
||||
<Route
|
||||
path="/chaintransactions"
|
||||
render={() => getGrid(ChainView)}
|
||||
/>
|
||||
<Route path="/settings" render={() => getGrid(SettingsView)} />
|
||||
<Route path="/fees" render={() => getGrid(FeesView)} />
|
||||
<Route path="/trading" render={() => getGrid(TraderView)} />
|
||||
<Route path="/terms" render={() => <TermsView />} />
|
||||
<Route path="/privacy" render={() => <PrivacyView />} />
|
||||
<Route path="/faq" render={() => <FaqView />} />
|
||||
<Route path="*" render={() => <NotFound />} />
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getGrid = (Content: any) => (
|
||||
<Section padding={'16px 0 32px'}>
|
||||
<Container>
|
||||
<Navigation />
|
||||
<ContentStyle>
|
||||
<Content />
|
||||
</ContentStyle>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default Content;
|
|
@ -1,158 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
headerColor,
|
||||
headerTextColor,
|
||||
fontColors,
|
||||
mediaWidths,
|
||||
} from 'styles/Themes';
|
||||
import { Section } from 'components/section/Section';
|
||||
import { Link } from 'components/link/Link';
|
||||
import { Emoji } from 'components/emoji/Emoji';
|
||||
import { useAccount } from 'context/AccountContext';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { HomeButton } from 'views/entry/homepage/HomePage.styled';
|
||||
import { Zap } from 'components/generic/Icons';
|
||||
|
||||
const FooterStyle = styled.div`
|
||||
padding: 40px 0;
|
||||
min-height: 300px;
|
||||
color: ${headerTextColor};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
padding: 0 0 40px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const SideFooter = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const RightFooter = styled(SideFooter)`
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
width: 80%;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-top: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-weight: 900;
|
||||
color: ${headerTextColor};
|
||||
`;
|
||||
|
||||
const SideText = styled.p`
|
||||
font-size: 14px;
|
||||
color: ${fontColors.grey7};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const CopyrightText = styled(SideText)`
|
||||
font-size: 12px;
|
||||
color: ${fontColors.blue};
|
||||
`;
|
||||
|
||||
const StyledRouter = styled(RouterLink)`
|
||||
margin-top: 12px;
|
||||
|
||||
${HomeButton} {
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
const Version = styled.div`
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
const APP_VERSION = process.env.REACT_APP_VERSION || '0.0.0';
|
||||
|
||||
export const Footer = () => {
|
||||
const { loggedIn } = useAccount();
|
||||
return (
|
||||
<Section withColor={true} color={headerColor}>
|
||||
<FooterStyle>
|
||||
<SideFooter>
|
||||
<Line>
|
||||
<RouterLink to="/" style={{ textDecoration: 'none' }}>
|
||||
<Title>ThunderHub</Title>
|
||||
</RouterLink>
|
||||
<Version>{`v${APP_VERSION}`}</Version>
|
||||
</Line>
|
||||
<SideText>
|
||||
Open-source lightning node manager to control and
|
||||
monitor your LND node.
|
||||
</SideText>
|
||||
<SideText>
|
||||
Made in Munich with{' '}
|
||||
<Emoji symbol={'🧡'} label={'heart'} /> and{' '}
|
||||
<Emoji symbol={'⚡'} label={'lightning'} />.
|
||||
</SideText>
|
||||
<CopyrightText>
|
||||
Copyright © 2020. All rights reserved. ThunderHub
|
||||
</CopyrightText>
|
||||
</SideFooter>
|
||||
<RightFooter>
|
||||
<Link to={'/faq'} color={fontColors.blue}>
|
||||
FAQ
|
||||
</Link>
|
||||
<Link
|
||||
href={'https://github.com/apotdevin/thunderhub'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
<Link
|
||||
href={'https://twitter.com/thunderhubio'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
<Link to={'/terms'} color={fontColors.blue}>
|
||||
Terms of Use
|
||||
</Link>
|
||||
<Link to={'/privacy'} color={fontColors.blue}>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
{!loggedIn && (
|
||||
<StyledRouter
|
||||
to="/login"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<HomeButton>
|
||||
<Zap fillcolor={'white'} color={'white'} />
|
||||
LOGIN
|
||||
</HomeButton>
|
||||
</StyledRouter>
|
||||
)}
|
||||
</RightFooter>
|
||||
</FooterStyle>
|
||||
</Section>
|
||||
);
|
||||
};
|
|
@ -1,173 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
headerColor,
|
||||
headerTextColor,
|
||||
themeColors,
|
||||
mediaWidths,
|
||||
mediaDimensions,
|
||||
} from '../../styles/Themes';
|
||||
import { HomeButton } from '../../views/entry/homepage/HomePage.styled';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAccount } from '../../context/AccountContext';
|
||||
import { SingleLine, ResponsiveLine } from '../../components/generic/Styled';
|
||||
import {
|
||||
Cpu,
|
||||
MenuIcon,
|
||||
XSvg,
|
||||
Zap,
|
||||
Circle,
|
||||
} from '../../components/generic/Icons';
|
||||
import { BurgerMenu } from 'components/burgerMenu/BurgerMenu';
|
||||
import { useSize } from 'hooks/UseSize';
|
||||
import { useTransition, animated } from 'react-spring';
|
||||
import { Section } from 'components/section/Section';
|
||||
import { useStatusState } from 'context/StatusContext';
|
||||
|
||||
const HeaderStyle = styled.div`
|
||||
padding: 16px 0;
|
||||
`;
|
||||
|
||||
const IconPadding = styled.div`
|
||||
padding-right: 6px;
|
||||
margin-bottom: -4px;
|
||||
`;
|
||||
|
||||
const HeaderTitle = styled.div`
|
||||
color: ${headerTextColor};
|
||||
font-weight: 900;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${({ withPadding }: { withPadding: boolean }) =>
|
||||
withPadding &&
|
||||
css`
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const LinkWrapper = styled.div`
|
||||
color: ${headerTextColor};
|
||||
margin: ${({ last }: { last?: boolean }) =>
|
||||
last ? '0 16px 0 4px' : '0 4px'};
|
||||
|
||||
:hover {
|
||||
color: ${themeColors.blue2};
|
||||
}
|
||||
`;
|
||||
|
||||
const AnimatedBurger = animated(MenuIcon);
|
||||
const AnimatedClose = animated(XSvg);
|
||||
|
||||
export const Header = () => {
|
||||
const { width } = useSize();
|
||||
const { loggedIn } = useAccount();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { syncedToChain } = useStatusState();
|
||||
|
||||
const transitions = useTransition(open, null, {
|
||||
from: { position: 'absolute', opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
});
|
||||
|
||||
const renderLoggedIn = () => {
|
||||
if (width <= mediaDimensions.mobile) {
|
||||
return (
|
||||
<IconWrapper onClick={() => setOpen((prev) => !prev)}>
|
||||
{transitions.map(({ item, key, props }) =>
|
||||
item ? (
|
||||
<AnimatedClose
|
||||
key={key}
|
||||
style={props}
|
||||
size={'24px'}
|
||||
/>
|
||||
) : (
|
||||
<AnimatedBurger
|
||||
key={key}
|
||||
style={props}
|
||||
size={'24px'}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</IconWrapper>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Circle
|
||||
size={'12px'}
|
||||
strokeWidth={'0'}
|
||||
fillcolor={syncedToChain ? '#95de64' : '#ff7875'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLoggedOut = () => (
|
||||
<>
|
||||
<Link to="/faq" style={{ textDecoration: 'none' }}>
|
||||
<LinkWrapper>Faq</LinkWrapper>
|
||||
</Link>
|
||||
<Link to="/terms" style={{ textDecoration: 'none' }}>
|
||||
<LinkWrapper>Terms</LinkWrapper>
|
||||
</Link>
|
||||
<Link to="/privacy" style={{ textDecoration: 'none' }}>
|
||||
<LinkWrapper last={true}>Privacy</LinkWrapper>
|
||||
</Link>
|
||||
<Link to="/login" style={{ textDecoration: 'none' }}>
|
||||
<HomeButton>
|
||||
<Zap fillcolor={'white'} color={'white'} />
|
||||
</HomeButton>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
const HeaderWrapper =
|
||||
width <= mediaDimensions.mobile && !loggedIn
|
||||
? ResponsiveLine
|
||||
: SingleLine;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
withColor={true}
|
||||
color={headerColor}
|
||||
textColor={headerTextColor}
|
||||
>
|
||||
<HeaderStyle>
|
||||
<HeaderWrapper>
|
||||
<Link to="/" style={{ textDecoration: 'none' }}>
|
||||
<HeaderTitle
|
||||
withPadding={
|
||||
width <= mediaDimensions.mobile && !loggedIn
|
||||
}
|
||||
>
|
||||
<IconPadding>
|
||||
<Cpu color={'white'} />
|
||||
</IconPadding>
|
||||
ThunderHub
|
||||
</HeaderTitle>
|
||||
</Link>
|
||||
<SingleLine>
|
||||
{loggedIn ? renderLoggedIn() : renderLoggedOut()}
|
||||
</SingleLine>
|
||||
</HeaderWrapper>
|
||||
</HeaderStyle>
|
||||
</Section>
|
||||
{open && width <= mediaDimensions.mobile && (
|
||||
<BurgerMenu open={open} setOpen={setOpen} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,211 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { NodeInfo } from './nodeInfo/NodeInfo';
|
||||
import { SideSettings } from './sideSettings/SideSettings';
|
||||
import {
|
||||
unSelectedNavButton,
|
||||
navBackgroundColor,
|
||||
navTextColor,
|
||||
subCardColor,
|
||||
cardBorderColor,
|
||||
mediaWidths,
|
||||
} from '../../styles/Themes';
|
||||
import {
|
||||
Home,
|
||||
Cpu,
|
||||
Server,
|
||||
Settings,
|
||||
Shield,
|
||||
Crosshair,
|
||||
GitPullRequest,
|
||||
LinkIcon,
|
||||
RepeatIcon,
|
||||
Users,
|
||||
CreditCard,
|
||||
} from '../../components/generic/Icons';
|
||||
import { useSettings } from '../../context/SettingsContext';
|
||||
import { useConnectionState } from 'context/ConnectionContext';
|
||||
|
||||
const NavigationStyle = styled.div`
|
||||
grid-area: nav;
|
||||
width: ${({ isOpen }: { isOpen: boolean }) => (isOpen ? '200px' : '60px')};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StickyCard = styled.div`
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
`;
|
||||
|
||||
const LinkView = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
const ButtonSection = styled.div`
|
||||
width: 100%;
|
||||
${({ isOpen }: { isOpen: boolean }) =>
|
||||
!isOpen &&
|
||||
css`
|
||||
margin: 8px 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
const NavSeparation = styled.div`
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
interface NavProps {
|
||||
selected: boolean;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
const NavButton = styled(({ isOpen, ...rest }) => <Link {...rest} />)(
|
||||
() => css`
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background: ${({ selected }: NavProps) =>
|
||||
selected && navBackgroundColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${({ isOpen }: NavProps) => !isOpen && 'justify-content: center'};
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin: 4px 0;
|
||||
color: ${({ selected }: NavProps) =>
|
||||
selected ? navTextColor : unSelectedNavButton};
|
||||
|
||||
&:hover {
|
||||
color: ${navTextColor};
|
||||
background: ${navBackgroundColor};
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const BurgerRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: scroll;
|
||||
background: ${cardBorderColor};
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const BurgerNav = styled(({ selectedColor, ...rest }) => <Link {...rest} />)(
|
||||
() => css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 16px 8px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
background: ${({ selected }: NavProps) => selected && subCardColor};
|
||||
${({ isOpen }: NavProps) => !isOpen && 'justify-content: center'};
|
||||
color: ${({ selected }: NavProps) =>
|
||||
selected ? navTextColor : unSelectedNavButton};
|
||||
`,
|
||||
);
|
||||
|
||||
const HOME = '/';
|
||||
const PEERS = '/peers';
|
||||
const CHANNEL = '/channels';
|
||||
const BALANCE = '/balance';
|
||||
const TRANS = '/transactions';
|
||||
const FORWARDS = '/forwards';
|
||||
const CHAIN_TRANS = '/chainTransactions';
|
||||
const TOOLS = '/tools';
|
||||
const SETTINGS = '/settings';
|
||||
const FEES = '/fees';
|
||||
const TRADER = '/trading';
|
||||
|
||||
interface NavigationProps {
|
||||
isBurger?: boolean;
|
||||
setOpen?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const { sidebar, setSettings } = useSettings();
|
||||
const { connected } = useConnectionState();
|
||||
|
||||
const renderNavButton = (
|
||||
title: string,
|
||||
link: string,
|
||||
NavIcon: any,
|
||||
open: boolean = true,
|
||||
) => (
|
||||
<NavButton isOpen={sidebar} selected={pathname === link} to={link}>
|
||||
<NavIcon />
|
||||
{open && <NavSeparation>{title}</NavSeparation>}
|
||||
</NavButton>
|
||||
);
|
||||
|
||||
const renderBurgerNav = (title: string, link: string, NavIcon: any) => (
|
||||
<BurgerNav
|
||||
selected={pathname === link}
|
||||
to={link}
|
||||
onClick={() => setOpen && setOpen(false)}
|
||||
>
|
||||
<NavIcon />
|
||||
{title}
|
||||
</BurgerNav>
|
||||
);
|
||||
|
||||
const renderLinks = () => (
|
||||
<ButtonSection isOpen={sidebar}>
|
||||
{renderNavButton('Home', HOME, Home, sidebar)}
|
||||
{renderNavButton('Peers', PEERS, Users, sidebar)}
|
||||
{renderNavButton('Channels', CHANNEL, Cpu, sidebar)}
|
||||
{renderNavButton('Balance', BALANCE, RepeatIcon, sidebar)}
|
||||
{renderNavButton('Fees', FEES, Crosshair, sidebar)}
|
||||
{renderNavButton('Transactions', TRANS, Server, sidebar)}
|
||||
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
|
||||
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
|
||||
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
|
||||
{renderNavButton('P2P Trading', TRADER, CreditCard, sidebar)}
|
||||
{renderNavButton('Settings', SETTINGS, Settings, sidebar)}
|
||||
</ButtonSection>
|
||||
);
|
||||
|
||||
const renderBurger = () => (
|
||||
<BurgerRow>
|
||||
{renderBurgerNav('Home', HOME, Home)}
|
||||
{renderBurgerNav('Peers', PEERS, Users)}
|
||||
{renderBurgerNav('Channels', CHANNEL, Cpu)}
|
||||
{renderBurgerNav('Balance', BALANCE, RepeatIcon)}
|
||||
{renderBurgerNav('Fees', FEES, Crosshair)}
|
||||
{renderBurgerNav('Transactions', TRANS, Server)}
|
||||
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
|
||||
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
|
||||
{renderBurgerNav('Tools', TOOLS, Shield)}
|
||||
{renderBurgerNav('Trading', TRADER, CreditCard)}
|
||||
{renderBurgerNav('Settings', SETTINGS, Settings)}
|
||||
</BurgerRow>
|
||||
);
|
||||
|
||||
if (isBurger) {
|
||||
return renderBurger();
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationStyle isOpen={sidebar}>
|
||||
<StickyCard>
|
||||
<LinkView>
|
||||
{connected && <NodeInfo isOpen={sidebar} />}
|
||||
{renderLinks()}
|
||||
<SideSettings isOpen={sidebar} setIsOpen={setSettings} />
|
||||
</LinkView>
|
||||
</StickyCard>
|
||||
</NavigationStyle>
|
||||
);
|
||||
};
|
|
@ -1,295 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { GET_NODE_INFO } from '../../../graphql/query';
|
||||
import { useSettings } from '../../../context/SettingsContext';
|
||||
import {
|
||||
Separation,
|
||||
SingleLine,
|
||||
SubTitle,
|
||||
Sub4Title,
|
||||
} from '../../../components/generic/Styled';
|
||||
import {
|
||||
QuestionIcon,
|
||||
Zap,
|
||||
Anchor,
|
||||
Circle,
|
||||
} from '../../../components/generic/Icons';
|
||||
import { getTooltipType } from '../../../components/generic/Helpers';
|
||||
import { useAccount } from '../../../context/AccountContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
import { textColorMap, unSelectedNavButton } from '../../../styles/Themes';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import styled from 'styled-components';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getPrice } from 'components/price/Price';
|
||||
import { AnimatedNumber } from 'components/animated/AnimatedNumber';
|
||||
import { useStatusState } from 'context/StatusContext';
|
||||
import { usePriceState } from 'context/PriceContext';
|
||||
|
||||
const Closed = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Margin = styled.div`
|
||||
margin: 8px 0 2px;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Info = styled.div`
|
||||
font-size: 14px;
|
||||
color: #bfbfbf;
|
||||
border-bottom: 2px solid
|
||||
${({ bottomColor }: { bottomColor: string }) => bottomColor};
|
||||
`;
|
||||
|
||||
const Balance = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2px 0;
|
||||
padding: 0 5px;
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const Alias = styled.div`
|
||||
border-bottom: 2px solid
|
||||
${({ bottomColor }: { bottomColor: string }) => bottomColor};
|
||||
`;
|
||||
|
||||
interface NodeInfoProps {
|
||||
isOpen?: boolean;
|
||||
isBurger?: boolean;
|
||||
}
|
||||
|
||||
export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
|
||||
const {
|
||||
syncedToChain,
|
||||
chainBalance,
|
||||
chainPending,
|
||||
channelBalance,
|
||||
channelPending,
|
||||
} = useStatusState();
|
||||
|
||||
const { host, viewOnly, cert, sessionAdmin } = useAccount();
|
||||
const auth = {
|
||||
host,
|
||||
macaroon: viewOnly !== '' ? viewOnly : sessionAdmin,
|
||||
cert,
|
||||
};
|
||||
|
||||
const { loading, data } = useQuery(GET_NODE_INFO, {
|
||||
variables: { auth },
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
const { theme, currency } = useSettings();
|
||||
const priceContext = usePriceState();
|
||||
const format = getPrice(currency, priceContext);
|
||||
|
||||
const tooltipType = getTooltipType(theme);
|
||||
|
||||
if (loading || !data || !data.getNodeInfo) {
|
||||
return (
|
||||
<Closed>
|
||||
<ScaleLoader
|
||||
height={10}
|
||||
width={2}
|
||||
color={textColorMap[theme]}
|
||||
/>
|
||||
</Closed>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
color,
|
||||
active_channels_count,
|
||||
closed_channels_count,
|
||||
alias,
|
||||
peers_count,
|
||||
pending_channels_count,
|
||||
version,
|
||||
} = data.getNodeInfo;
|
||||
|
||||
const formatCB = format({ amount: chainBalance });
|
||||
const formatPB = format({ amount: chainPending });
|
||||
const formatCCB = format({ amount: channelBalance });
|
||||
const formatPCB = format({ amount: channelPending });
|
||||
|
||||
if (isBurger) {
|
||||
return (
|
||||
<>
|
||||
<SingleLine>
|
||||
<SubTitle>{alias}</SubTitle>
|
||||
<Circle
|
||||
strokeWidth={'0'}
|
||||
fillcolor={syncedToChain ? '#95de64' : '#ff7875'}
|
||||
/>
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Sub4Title>Channels</Sub4Title>
|
||||
{`${active_channels_count} / ${pending_channels_count} / ${closed_channels_count} / ${peers_count}`}
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Zap
|
||||
color={channelPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
fillcolor={channelPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
/>
|
||||
{channelPending > 0 ? (
|
||||
`${formatCCB} / ${formatPCB}`
|
||||
) : (
|
||||
<AnimatedNumber amount={channelBalance} />
|
||||
)}
|
||||
</SingleLine>
|
||||
<SingleLine>
|
||||
<Anchor
|
||||
color={chainPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
/>
|
||||
{chainPending > 0 ? (
|
||||
`${formatCB} / ${formatPB}`
|
||||
) : (
|
||||
<AnimatedNumber amount={chainBalance} />
|
||||
)}
|
||||
</SingleLine>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<>
|
||||
<Closed>
|
||||
<div data-tip data-for="full_balance_tip">
|
||||
<Circle
|
||||
strokeWidth={'0'}
|
||||
fillcolor={syncedToChain ? '#95de64' : '#ff7875'}
|
||||
/>
|
||||
{(channelPending > 0 || chainPending > 0) && (
|
||||
<div>
|
||||
<Circle
|
||||
fillcolor={'#652EC7'}
|
||||
strokeWidth={'0'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Margin>
|
||||
<Zap
|
||||
fillcolor={
|
||||
channelPending === 0 ? '#FFD300' : '#652EC7'
|
||||
}
|
||||
color={
|
||||
channelPending === 0 ? '#FFD300' : '#652EC7'
|
||||
}
|
||||
/>
|
||||
</Margin>
|
||||
<Anchor
|
||||
color={chainPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
/>
|
||||
</div>
|
||||
<div data-tip data-for="full_node_tip">
|
||||
<SingleLine>{active_channels_count}</SingleLine>
|
||||
<SingleLine>{pending_channels_count}</SingleLine>
|
||||
<SingleLine>{closed_channels_count}</SingleLine>
|
||||
<SingleLine>{peers_count}</SingleLine>
|
||||
</div>
|
||||
</Closed>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<ReactTooltip
|
||||
id={'full_balance_tip'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
<div>{`Channel Balance: ${formatCCB}`}</div>
|
||||
<div>{`Pending Channel Balance: ${formatPCB}`}</div>
|
||||
<div>{`Chain Balance: ${formatCB}`}</div>
|
||||
<div>{`Pending Chain Balance: ${formatPB}`}</div>
|
||||
</ReactTooltip>
|
||||
<ReactTooltip
|
||||
id={'full_node_tip'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
<div>{`Active Channels: ${active_channels_count}`}</div>
|
||||
<div>{`Pending Channels: ${pending_channels_count}`}</div>
|
||||
<div>{`Closed Channels: ${closed_channels_count}`}</div>
|
||||
<div>{`Peers: ${peers_count}`}</div>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<Alias bottomColor={color}>{alias}</Alias>
|
||||
{isOpen && (
|
||||
<QuestionIcon
|
||||
data-tip={`Version: ${version.split(' ')[0]}`}
|
||||
/>
|
||||
)}
|
||||
</Title>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<Balance data-tip data-for="balance_tip">
|
||||
<Zap color={channelPending === 0 ? '#FFD300' : '#652EC7'} />
|
||||
<AnimatedNumber amount={channelBalance} />
|
||||
</Balance>
|
||||
<Balance data-tip data-for="chain_balance_tip">
|
||||
<Anchor color={chainPending === 0 ? '#FFD300' : '#652EC7'} />
|
||||
<AnimatedNumber amount={chainBalance} />
|
||||
</Balance>
|
||||
<Balance
|
||||
data-tip
|
||||
data-for="node_tip"
|
||||
>{`${active_channels_count} / ${pending_channels_count} / ${closed_channels_count} / ${peers_count}`}</Balance>
|
||||
<Balance>
|
||||
<Info bottomColor={syncedToChain ? '#95de64' : '#ff7875'}>
|
||||
{syncedToChain ? 'Synced' : 'Not Synced'}
|
||||
</Info>
|
||||
</Balance>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<ReactTooltip effect={'solid'} place={'right'} type={tooltipType} />
|
||||
<ReactTooltip
|
||||
id={'balance_tip'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
<div>{`Channel Balance: ${formatCCB}`}</div>
|
||||
<div>{`Pending Channel Balance: ${formatPCB}`}</div>
|
||||
</ReactTooltip>
|
||||
<ReactTooltip
|
||||
id={'chain_balance_tip'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
<div>{`Chain Balance: ${formatCB}`}</div>
|
||||
<div>{`Pending Chain Balance: ${formatPB}`}</div>
|
||||
</ReactTooltip>
|
||||
<ReactTooltip
|
||||
id={'node_tip'}
|
||||
effect={'solid'}
|
||||
place={'right'}
|
||||
type={tooltipType}
|
||||
>
|
||||
<div>{`Active Channels: ${active_channels_count}`}</div>
|
||||
<div>{`Pending Channels: ${pending_channels_count}`}</div>
|
||||
<div>{`Closed Channels: ${closed_channels_count}`}</div>
|
||||
<div>{`Peers: ${peers_count}`}</div>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,206 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Separation, SingleLine } from '../../../components/generic/Styled';
|
||||
import { useSettings } from '../../../context/SettingsContext';
|
||||
import {
|
||||
Sun,
|
||||
Moon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from '../../../components/generic/Icons';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
progressBackground,
|
||||
iconButtonHover,
|
||||
inverseTextColor,
|
||||
unSelectedNavButton,
|
||||
} from '../../../styles/Themes';
|
||||
|
||||
const SelectedIcon = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 100%;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: 579px) {
|
||||
&:hover {
|
||||
background-color: ${iconButtonHover};
|
||||
color: ${inverseTextColor};
|
||||
}
|
||||
}
|
||||
background-color: ${({ selected }: { selected: boolean }) =>
|
||||
selected ? progressBackground : ''};
|
||||
`;
|
||||
|
||||
const Symbol = styled.div`
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const IconRow = styled.div`
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
${({ center }: { center?: boolean }) => center && 'width: 100%'}
|
||||
`;
|
||||
|
||||
const BurgerPadding = styled(SingleLine)`
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
const currencyArray = ['sat', 'btc', 'EUR', 'USD'];
|
||||
const themeArray = ['light', 'dark'];
|
||||
const currencyMap: { [key: string]: string } = {
|
||||
sat: 'S',
|
||||
btc: '₿',
|
||||
EUR: '€',
|
||||
USD: '$',
|
||||
};
|
||||
const themeMap: { [key: string]: string } = {
|
||||
light: Sun,
|
||||
dark: Moon,
|
||||
};
|
||||
|
||||
const getNextValue = (array: string[], current: string): string => {
|
||||
const length = array.length;
|
||||
const index = array.indexOf(current);
|
||||
|
||||
let value = '';
|
||||
if (index + 1 === length) {
|
||||
value = array[0];
|
||||
} else {
|
||||
value = array[index + 1];
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
interface SideSettingsProps {
|
||||
isOpen?: boolean;
|
||||
isBurger?: boolean;
|
||||
setIsOpen?: (state: any) => void;
|
||||
}
|
||||
|
||||
export const SideSettings = ({
|
||||
isOpen,
|
||||
isBurger,
|
||||
setIsOpen,
|
||||
}: SideSettingsProps) => {
|
||||
const { theme, currency, setSettings } = useSettings();
|
||||
|
||||
const renderIcon = (
|
||||
type: string,
|
||||
value: string,
|
||||
text: string,
|
||||
on: boolean = false,
|
||||
Icon?: any,
|
||||
) => (
|
||||
<SelectedIcon
|
||||
selected={
|
||||
(type === 'currency' ? currency === value : theme === value) ||
|
||||
on
|
||||
}
|
||||
onClick={() => {
|
||||
localStorage.setItem(type, value);
|
||||
type === 'currency' &&
|
||||
setSettings({
|
||||
currency:
|
||||
isOpen || isBurger
|
||||
? value
|
||||
: getNextValue(currencyArray, value),
|
||||
});
|
||||
type === 'theme' && setSettings({ theme: value });
|
||||
}}
|
||||
>
|
||||
{type === 'currency' && <Symbol>{text}</Symbol>}
|
||||
{type === 'theme' && <Icon />}
|
||||
</SelectedIcon>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<IconRow center={true}>
|
||||
{renderIcon(
|
||||
'currency',
|
||||
currency,
|
||||
currencyMap[currency],
|
||||
true,
|
||||
)}
|
||||
</IconRow>
|
||||
<IconRow center={true}>
|
||||
{renderIcon(
|
||||
'theme',
|
||||
getNextValue(themeArray, theme),
|
||||
'',
|
||||
true,
|
||||
themeMap[getNextValue(themeArray, theme)],
|
||||
)}
|
||||
</IconRow>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<IconRow>
|
||||
{renderIcon('currency', 'sat', 'S')}
|
||||
{renderIcon('currency', 'btc', '₿')}
|
||||
{renderIcon('currency', 'EUR', '€')}
|
||||
{renderIcon('currency', 'USD', '$')}
|
||||
</IconRow>
|
||||
<IconRow>
|
||||
{renderIcon('theme', 'light', '', false, Sun)}
|
||||
{renderIcon('theme', 'dark', '', false, Moon)}
|
||||
</IconRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isBurger) {
|
||||
return (
|
||||
<BurgerPadding>
|
||||
<IconRow>
|
||||
{renderIcon('currency', 'sat', 'S')}
|
||||
{renderIcon('currency', 'btc', '₿')}
|
||||
{renderIcon('currency', 'EUR', '€')}
|
||||
{renderIcon('currency', 'USD', '$')}
|
||||
</IconRow>
|
||||
<IconRow>
|
||||
{renderIcon('theme', 'light', '', false, Sun)}
|
||||
{renderIcon('theme', 'dark', '', false, Moon)}
|
||||
</IconRow>
|
||||
</BurgerPadding>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderContent()}
|
||||
{setIsOpen && (
|
||||
<IconRow center={!isOpen}>
|
||||
<SelectedIcon
|
||||
selected={true}
|
||||
onClick={() => {
|
||||
localStorage.setItem(
|
||||
'sidebar',
|
||||
(!isOpen).toString(),
|
||||
);
|
||||
setIsOpen({ sidebar: !isOpen });
|
||||
}}
|
||||
>
|
||||
{isOpen ? <ChevronLeft /> : <ChevronRight />}
|
||||
</SelectedIcon>
|
||||
</IconRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,144 +0,0 @@
|
|||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
);
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
||||
window.location.href,
|
||||
);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null &&
|
||||
contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-ExtraBold.otf')
|
||||
format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-Bold.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-SemiBold.otf')
|
||||
format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-Medium.otf')
|
||||
format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-Regular.otf')
|
||||
format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-Light.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url('../assets/fonts/Manrope/otf/Manrope-ExtraLight.otf')
|
||||
format('opentype');
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
import base64url from 'base64url';
|
||||
import { saveAccounts } from './storage';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
const THUNDERHUB_NAMESPACE = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
interface BuildProps {
|
||||
name?: string;
|
||||
host: string;
|
||||
admin?: string;
|
||||
viewOnly?: string;
|
||||
cert?: string;
|
||||
accounts: any;
|
||||
}
|
||||
|
||||
export const saveUserAuth = ({
|
||||
name = '',
|
||||
host,
|
||||
admin = '',
|
||||
viewOnly = '',
|
||||
cert = '',
|
||||
accounts,
|
||||
}: BuildProps) => {
|
||||
const id = getAccountId(host, viewOnly, admin, cert);
|
||||
const newAccount = {
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id,
|
||||
};
|
||||
|
||||
const newAccounts = [...accounts, newAccount];
|
||||
saveAccounts(newAccounts);
|
||||
};
|
||||
|
||||
export const getAccountId = (
|
||||
host: string = '',
|
||||
viewOnly: string = '',
|
||||
admin: string = '',
|
||||
cert: string = '',
|
||||
) =>
|
||||
uuidv5(
|
||||
`${host}-${viewOnly}-${admin !== '' ? 1 : 0}-${cert}`,
|
||||
THUNDERHUB_NAMESPACE,
|
||||
);
|
||||
|
||||
export const saveSessionAuth = (sessionAdmin: string) =>
|
||||
sessionStorage.setItem('session', sessionAdmin);
|
||||
|
||||
export const getAuth = (account?: string) => {
|
||||
const accounts = JSON.parse(localStorage.getItem('accounts') || '[]');
|
||||
const currentActive = Math.max(
|
||||
parseInt(account ?? (localStorage.getItem('active') || '0')),
|
||||
0,
|
||||
);
|
||||
const sessionAdmin = sessionStorage.getItem('session') || '';
|
||||
|
||||
const accountsLength = accounts.length;
|
||||
|
||||
const active =
|
||||
accountsLength > 0 && currentActive >= accountsLength
|
||||
? 0
|
||||
: currentActive;
|
||||
|
||||
const defaultAccount = {
|
||||
name: '',
|
||||
host: '',
|
||||
admin: '',
|
||||
viewOnly: '',
|
||||
cert: '',
|
||||
id: '',
|
||||
};
|
||||
|
||||
const activeAccount =
|
||||
accountsLength > 0 && active < accountsLength
|
||||
? accounts[active]
|
||||
: defaultAccount;
|
||||
|
||||
const { name, host, admin, viewOnly, cert, id } = activeAccount;
|
||||
const currentId =
|
||||
id ??
|
||||
uuidv5(
|
||||
`${host}-${viewOnly}-${admin !== '' ? 1 : 0}-${cert}`,
|
||||
THUNDERHUB_NAMESPACE,
|
||||
);
|
||||
const loggedIn = host !== '' && (viewOnly !== '' || sessionAdmin !== '');
|
||||
|
||||
return {
|
||||
name,
|
||||
host,
|
||||
admin,
|
||||
viewOnly,
|
||||
cert,
|
||||
id: currentId,
|
||||
accounts,
|
||||
loggedIn,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAuthLnd = (lndconnect: string) => {
|
||||
const auth = lndconnect.replace('lndconnect', 'https');
|
||||
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(auth);
|
||||
} catch (error) {
|
||||
return {
|
||||
cert: '',
|
||||
macaroon: '',
|
||||
socket: '',
|
||||
};
|
||||
}
|
||||
|
||||
const cert = url.searchParams.get('cert') || '';
|
||||
const macaroon = url.searchParams.get('macaroon') || '';
|
||||
const socket = url.host;
|
||||
|
||||
return {
|
||||
cert: base64url.toBase64(cert),
|
||||
macaroon: base64url.toBase64(macaroon),
|
||||
socket,
|
||||
};
|
||||
};
|
||||
|
||||
export const getBase64CertfromDerFormat = (base64: string) => {
|
||||
if (!base64) return null;
|
||||
|
||||
const prefix = '-----BEGIN CERTIFICATE-----\n';
|
||||
const postfix = '-----END CERTIFICATE-----';
|
||||
const pem = base64.match(/.{0,64}/g) || [];
|
||||
const pemString = pem.join('\n');
|
||||
const pemComplete = prefix + pemString + postfix;
|
||||
const pemText = base64url.encode(pemComplete);
|
||||
|
||||
return pemText;
|
||||
};
|
||||
|
||||
const emptyObject = {
|
||||
cert: undefined,
|
||||
admin: undefined,
|
||||
viewOnly: undefined,
|
||||
host: undefined,
|
||||
};
|
||||
|
||||
export const getConfigLnd = (json: string) => {
|
||||
const parsedJson = JSON.parse(json);
|
||||
|
||||
const config = parsedJson.configurations;
|
||||
|
||||
if (config && config.length >= 1) {
|
||||
const cert = config[0].certificateThumbprint || '';
|
||||
const admin = config[0].adminMacaroon;
|
||||
const viewOnly = config[0].readonlyMacaroon;
|
||||
const host = config[0].host;
|
||||
const port = config[0].port;
|
||||
|
||||
return {
|
||||
cert,
|
||||
admin,
|
||||
viewOnly,
|
||||
host: `${host}:${port}`,
|
||||
};
|
||||
}
|
||||
|
||||
return emptyObject;
|
||||
};
|
||||
|
||||
export const getQRConfig = (json: string) => {
|
||||
const config = JSON.parse(json);
|
||||
|
||||
if (config) {
|
||||
const { name = '', cert = '', admin, viewOnly, host } = config;
|
||||
|
||||
return {
|
||||
name,
|
||||
cert,
|
||||
admin,
|
||||
viewOnly,
|
||||
host,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...emptyObject, name: undefined };
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue