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:
Anthony Potdevin 2020-04-12 18:27:01 +02:00 committed by GitHub
parent d0f6a038a9
commit aa60d618f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
577 changed files with 21212 additions and 28048 deletions

11
.babelrc Normal file
View 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
View file

@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

12
.dockerignore Normal file
View file

@ -0,0 +1,12 @@
.git
.gitignore
.cache
*.md
!README*.md
/node_modules
/.next
/docs
/.github
.env
.vscode
CHANGELOG.md

32
.gitignore vendored
View file

@ -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
View file

@ -0,0 +1,2 @@
/.next
/.node_modules

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 80,
"arrowParens": "avoid"
}

24
.storybook/config.js Normal file
View 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
View file

@ -0,0 +1,7 @@
module.exports = {
addons: [
'@storybook/addon-knobs/register',
'@storybook/addon-actions',
'@storybook/addon-viewport/register',
],
};

View file

@ -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',
});

View 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;

View 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
View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

5
@types/index.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.svg';
declare module '*.gif';

16
Dockerfile Normal file
View 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" ]

View file

@ -1,7 +1,7 @@
# **ThunderHub - Lightning Node Manager**
![Home Screenshot](assets/Home.png)
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=client/package.json)](https://snyk.io/test/github/apotdevin/thunderhub) [![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=server/package.json)](https://snyk.io/test/github/apotdevin/thunderhub) [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/)
![Home Screenshot](./docs/Home.png)
[![license](https://img.shields.io/github/license/DAVFoundation/captain-n3m0.svg?style=flat-square)](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
[![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=client/package.json)](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
[![Known Vulnerabilities](https://snyk.io/test/github/apotdevin/thunderhub/badge.svg?targetFile=server/package.json)](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.

View file

@ -1,4 +0,0 @@
node_modules
.git
.gitignore
build

View file

@ -1 +0,0 @@
REACT_APP_VERSION=$npm_package_version

27
client/.gitignore vendored
View file

@ -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*

View file

@ -1,7 +0,0 @@
{
"printWidth": 80,
"trailingComma": "all",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

View file

@ -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;
},
};

View file

@ -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);

View file

@ -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;

View file

@ -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"]

View file

@ -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;
}
}

View file

@ -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

View file

@ -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>

View file

@ -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"
}

View file

@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View file

@ -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);
});

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;
`;

View file

@ -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>
);
};

View file

@ -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()}
</>
);
};

View file

@ -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}
/>
)}
</>
);
};

View file

@ -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}
/>
</>
);
};

View file

@ -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>
);
};

View file

@ -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}
/>
</>
);
};

View file

@ -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}
/>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>;
};

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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;
};

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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);

View file

@ -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%;
}
`;

View file

@ -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')}
/>
);
};

View file

@ -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}
/>
);
};

View file

@ -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>;
};

View file

@ -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>
);
}
};

View file

@ -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>
);
};

View file

@ -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>
);

View file

@ -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;

View file

@ -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();
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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};
`;

View file

@ -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>
</>
);
};

View file

@ -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);
};

View file

@ -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} />;
};

View file

@ -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>;
};

View file

@ -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;
};

View file

@ -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>
);
};

View file

@ -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;
};

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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
}
}
}
`;

View file

@ -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
)
}
`;

View file

@ -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
}
}
`;

View file

@ -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();
};

View file

@ -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]);
};

View file

@ -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;
};

View file

@ -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();

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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} />
)}
</>
);
};

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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>
)}
</>
);
};

View file

@ -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();
});
}
}

View file

@ -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');
}

View file

@ -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