Merge remote-tracking branch 'thunderhub-client/master'
1
.env
Normal file
|
@ -0,0 +1 @@
|
||||||
|
REACT_APP_VERSION=$npm_package_version
|
23
.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
7
.prettierrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
25
.storybook/main.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
7
.storybook/manager.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { addons } from '@storybook/addons';
|
||||||
|
import { themes } from '@storybook/theming';
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: themes.dark,
|
||||||
|
panelPosition: 'right',
|
||||||
|
});
|
22
.storybook/preview.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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);
|
37
.storybook/themeDecorator.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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;
|
100
package.json
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "0.1.6.2",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/react-hooks": "^3.1.3",
|
||||||
|
"@types/crypto-js": "^3.1.43",
|
||||||
|
"@types/jest": "25.1.3",
|
||||||
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
"@types/lodash.merge": "^4.6.6",
|
||||||
|
"@types/lodash.sortby": "^4.7.6",
|
||||||
|
"@types/node": "13.7.4",
|
||||||
|
"@types/numeral": "^0.0.26",
|
||||||
|
"@types/qrcode.react": "^1.0.0",
|
||||||
|
"@types/react": "16.9.21",
|
||||||
|
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||||
|
"@types/react-dom": "16.9.5",
|
||||||
|
"@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": "^4.4.3",
|
||||||
|
"@types/styled-react-modal": "^1.2.0",
|
||||||
|
"@types/styled-theming": "^2.2.2",
|
||||||
|
"@types/victory": "^33.1.4",
|
||||||
|
"@types/zxcvbn": "^4.4.0",
|
||||||
|
"apollo-boost": "^0.4.4",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
|
"crypto-js": "^4.0.0",
|
||||||
|
"date-fns": "^2.8.0",
|
||||||
|
"graphql": "^14.6.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",
|
||||||
|
"react": "^16.11.0",
|
||||||
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
|
"react-dom": "^16.11.0",
|
||||||
|
"react-qr-reader": "^2.2.1",
|
||||||
|
"react-router-dom": "^5.1.2",
|
||||||
|
"react-scripts": "3.4.0",
|
||||||
|
"react-spinners": "^0.8.0",
|
||||||
|
"react-spring": "^8.0.27",
|
||||||
|
"react-toastify": "^5.4.1",
|
||||||
|
"react-tooltip": "^4.0.3",
|
||||||
|
"snyk": "^1.294.1",
|
||||||
|
"styled-components": "^5.0.1",
|
||||||
|
"styled-react-modal": "^2.0.0",
|
||||||
|
"styled-theming": "^2.2.0",
|
||||||
|
"typescript": "^3.7.2",
|
||||||
|
"victory": "^34.1.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-actions": "^5.3.13",
|
||||||
|
"@storybook/addon-info": "^5.3.13",
|
||||||
|
"@storybook/addon-knobs": "^5.3.13",
|
||||||
|
"@storybook/addon-links": "^5.3.13",
|
||||||
|
"@storybook/addon-viewport": "^5.3.13",
|
||||||
|
"@storybook/addons": "^5.3.13",
|
||||||
|
"@storybook/preset-create-react-app": "^1.5.2",
|
||||||
|
"@storybook/react": "^5.3.13",
|
||||||
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
|
"husky": "^4.2.3",
|
||||||
|
"prettier": "1.19.1",
|
||||||
|
"pretty-quick": "^2.0.1",
|
||||||
|
"react-docgen-typescript-loader": "^3.6.0"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "pretty-quick --staged"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
public/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 5.3 KiB |
60
public/index.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<!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>
|
20
public/manifest.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
9
src/App.test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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);
|
||||||
|
});
|
80
src/App.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
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.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:3001'
|
||||||
|
: 'https://api.thunderhub.io/',
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
BIN
src/assets/fonts/Manrope/otf/Manrope-Bold.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-ExtraBold.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-ExtraLight.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-Light.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-Medium.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-Regular.otf
Normal file
BIN
src/assets/fonts/Manrope/otf/Manrope-SemiBold.otf
Normal file
1
src/assets/icons/alert-circle.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
After Width: | Height: | Size: 356 B |
1
src/assets/icons/alert-triangle.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
After Width: | Height: | Size: 424 B |
1
src/assets/icons/anchor.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg>
|
After Width: | Height: | Size: 345 B |
1
src/assets/icons/arrow-down.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
|
After Width: | Height: | Size: 313 B |
1
src/assets/icons/arrow-up.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
|
After Width: | Height: | Size: 310 B |
1
src/assets/icons/check.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
After Width: | Height: | Size: 262 B |
1
src/assets/icons/chevron-down.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
After Width: | Height: | Size: 269 B |
1
src/assets/icons/chevron-left.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
After Width: | Height: | Size: 270 B |
1
src/assets/icons/chevron-right.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
After Width: | Height: | Size: 270 B |
1
src/assets/icons/chevron-up.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
After Width: | Height: | Size: 268 B |
1
src/assets/icons/chevrons-down.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg>
|
After Width: | Height: | Size: 317 B |
1
src/assets/icons/chevrons-up.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg>
|
After Width: | Height: | Size: 316 B |
1
src/assets/icons/circle.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg>
|
After Width: | Height: | Size: 258 B |
1
src/assets/icons/copy.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
After Width: | Height: | Size: 351 B |
1
src/assets/icons/cpu.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
After Width: | Height: | Size: 667 B |
1
src/assets/icons/crosshair.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crosshair"><circle cx="12" cy="12" r="10"></circle><line x1="22" y1="12" x2="18" y2="12"></line><line x1="6" y1="12" x2="2" y2="12"></line><line x1="12" y1="6" x2="12" y2="2"></line><line x1="12" y1="22" x2="12" y2="18"></line></svg>
|
After Width: | Height: | Size: 437 B |
1
src/assets/icons/edit.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
After Width: | Height: | Size: 365 B |
1
src/assets/icons/eye-off.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
After Width: | Height: | Size: 460 B |
1
src/assets/icons/eye.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
After Width: | Height: | Size: 316 B |
1
src/assets/icons/git-branch.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-branch"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>
|
After Width: | Height: | Size: 377 B |
1
src/assets/icons/git-commit.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-commit"><circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line></svg>
|
After Width: | Height: | Size: 358 B |
1
src/assets/icons/git-pull-request.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-pull-request"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M13 6h3a2 2 0 0 1 2 2v7"></path><line x1="6" y1="9" x2="6" y2="21"></line></svg>
|
After Width: | Height: | Size: 387 B |
1
src/assets/icons/github.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
After Width: | Height: | Size: 527 B |
1
src/assets/icons/globe.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
|
After Width: | Height: | Size: 409 B |
1
src/assets/icons/help-circle.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
After Width: | Height: | Size: 365 B |
1
src/assets/icons/home.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
After Width: | Height: | Size: 332 B |
1
src/assets/icons/key.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>
|
After Width: | Height: | Size: 352 B |
1
src/assets/icons/layers.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
After Width: | Height: | Size: 365 B |
1
src/assets/icons/link.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
After Width: | Height: | Size: 371 B |
1
src/assets/icons/loader.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg>
|
After Width: | Height: | Size: 614 B |
1
src/assets/icons/mail.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
After Width: | Height: | Size: 354 B |
1
src/assets/icons/menu.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
After Width: | Height: | Size: 346 B |
1
src/assets/icons/moon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
|
After Width: | Height: | Size: 281 B |
1
src/assets/icons/more-vertical.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
|
After Width: | Height: | Size: 341 B |
1
src/assets/icons/pocket.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pocket"><path d="M4 3h16a2 2 0 0 1 2 2v6a10 10 0 0 1-10 10A10 10 0 0 1 2 11V5a2 2 0 0 1 2-2z"></path><polyline points="8 10 12 14 16 10"></polyline></svg>
|
After Width: | Height: | Size: 358 B |
1
src/assets/icons/radio.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>
|
After Width: | Height: | Size: 389 B |
1
src/assets/icons/repeat.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
After Width: | Height: | Size: 392 B |
1
src/assets/icons/send.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
|
After Width: | Height: | Size: 314 B |
1
src/assets/icons/server.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-server"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
|
After Width: | Height: | Size: 431 B |
1
src/assets/icons/settings.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
After Width: | Height: | Size: 1,011 B |
1
src/assets/icons/shield.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
After Width: | Height: | Size: 279 B |
1
src/assets/icons/sliders.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>
|
After Width: | Height: | Size: 611 B |
1
src/assets/icons/sun.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
|
After Width: | Height: | Size: 650 B |
1
src/assets/icons/users.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
After Width: | Height: | Size: 400 B |
1
src/assets/icons/x.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
After Width: | Height: | Size: 299 B |
1
src/assets/icons/zap-off.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap-off"><polyline points="12.41 6.75 13 2 10.57 4.92"></polyline><polyline points="18.57 12.91 21 10 15.66 10"></polyline><polyline points="8 8 3 14 12 14 11 22 16 16"></polyline><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
After Width: | Height: | Size: 433 B |
1
src/assets/icons/zap.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
After Width: | Height: | Size: 282 B |
BIN
src/assets/images/Channels.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
src/assets/images/Forwards.png
Normal file
After Width: | Height: | Size: 156 KiB |
1
src/assets/images/MoshingDoodle.svg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
src/assets/images/NightLight.png
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/images/Reports.png
Normal file
After Width: | Height: | Size: 136 KiB |
1129
src/assets/images/ThunderHub.svg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
src/assets/images/Transactions.png
Normal file
After Width: | Height: | Size: 94 KiB |
15
src/components/adminSwitch/AdminSwitch.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useAccount } from '../../context/AccountContext';
|
||||||
|
|
||||||
|
interface AdminSwitchProps {
|
||||||
|
children: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminSwitch = ({ children }: AdminSwitchProps) => {
|
||||||
|
const { admin, sessionAdmin } = useAccount();
|
||||||
|
|
||||||
|
if (!admin && !sessionAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
10
src/components/animated/AnimatedNumber.stories.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { AnimatedNumber } from './AnimatedNumber';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Animated/Number',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => {
|
||||||
|
return <AnimatedNumber amount={100} />;
|
||||||
|
};
|
49
src/components/animated/AnimatedNumber.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
43
src/components/auth/Auth.styled.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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;
|
||||||
|
`;
|
44
src/components/auth/checks/AdminCheck.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { GET_CAN_ADMIN } from 'graphql/query';
|
||||||
|
import { useQuery } from '@apollo/react-hooks';
|
||||||
|
import { getAuthString } from 'utils/auth';
|
||||||
|
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: getAuthString(host, 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>
|
||||||
|
);
|
||||||
|
};
|
150
src/components/auth/checks/ViewCheck.tsx
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery } from '@apollo/react-hooks';
|
||||||
|
import { GET_CAN_CONNECT } from 'graphql/query';
|
||||||
|
import { getAuthString } from 'utils/auth';
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewCheck = ({
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
adminChecked,
|
||||||
|
setAdminChecked,
|
||||||
|
handleConnect,
|
||||||
|
callback,
|
||||||
|
}: ViewProps) => {
|
||||||
|
const [confirmed, setConfirmed] = useState(false);
|
||||||
|
|
||||||
|
const { data, loading } = useQuery(GET_CAN_CONNECT, {
|
||||||
|
variables: { auth: getAuthString(host, viewOnly ?? admin, cert) },
|
||||||
|
onCompleted: () => setConfirmed(true),
|
||||||
|
onError: () => setConfirmed(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
<AdminCheck
|
||||||
|
host={host}
|
||||||
|
admin={admin}
|
||||||
|
cert={cert}
|
||||||
|
setChecked={setAdminChecked}
|
||||||
|
/>
|
||||||
|
{renderButton()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
166
src/components/auth/index.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { getNextAvailable } from 'utils/storage';
|
||||||
|
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 } from 'utils/auth';
|
||||||
|
import { PasswordInput } from './views/Password';
|
||||||
|
import { useConnectionDispatch } from 'context/ConnectionContext';
|
||||||
|
import { useStatusDispatch } from 'context/StatusContext';
|
||||||
|
|
||||||
|
type AuthProps = {
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
callback: () => void;
|
||||||
|
setStatus: (state: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Auth = ({ type, status, callback, setStatus }: AuthProps) => {
|
||||||
|
const next = getNextAvailable();
|
||||||
|
|
||||||
|
const { changeAccount } = useAccount();
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
const dispatch = useConnectionDispatch();
|
||||||
|
const dispatchState = useStatusDispatch();
|
||||||
|
|
||||||
|
const [name, setName] = useState();
|
||||||
|
const [host, setHost] = useState();
|
||||||
|
const [admin, setAdmin] = useState();
|
||||||
|
const [viewOnly, setViewOnly] = useState();
|
||||||
|
const [cert, setCert] = useState();
|
||||||
|
const [password, setPassword] = useState();
|
||||||
|
|
||||||
|
const [adminChecked, setAdminChecked] = useState(false);
|
||||||
|
|
||||||
|
const handleSet = ({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
skipCheck,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
host?: string;
|
||||||
|
admin?: string;
|
||||||
|
viewOnly?: string;
|
||||||
|
cert?: string;
|
||||||
|
skipCheck?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (skipCheck) {
|
||||||
|
quickSave({ name, cert, admin, viewOnly, host });
|
||||||
|
} else {
|
||||||
|
name && setName(name);
|
||||||
|
host && setHost(host);
|
||||||
|
admin && setAdmin(admin);
|
||||||
|
viewOnly && setViewOnly(viewOnly);
|
||||||
|
cert && setCert(cert);
|
||||||
|
|
||||||
|
setStatus('confirmNode');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const quickSave = ({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
host?: string;
|
||||||
|
admin?: string;
|
||||||
|
viewOnly?: string;
|
||||||
|
cert?: string;
|
||||||
|
}) => {
|
||||||
|
saveUserAuth({
|
||||||
|
available: next,
|
||||||
|
name,
|
||||||
|
host: host || '',
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: 'disconnected' });
|
||||||
|
dispatchState({ type: 'disconnected' });
|
||||||
|
changeAccount(next);
|
||||||
|
|
||||||
|
push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const encryptedAdmin =
|
||||||
|
admin && password
|
||||||
|
? CryptoJS.AES.encrypt(admin, password).toString()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
saveUserAuth({
|
||||||
|
available: next,
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin: encryptedAdmin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: 'disconnected' });
|
||||||
|
dispatchState({ type: 'disconnected' });
|
||||||
|
changeAccount(next);
|
||||||
|
|
||||||
|
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' && (
|
||||||
|
<ViewCheck
|
||||||
|
host={host}
|
||||||
|
admin={admin}
|
||||||
|
viewOnly={viewOnly}
|
||||||
|
cert={cert}
|
||||||
|
adminChecked={adminChecked}
|
||||||
|
setAdminChecked={setAdminChecked}
|
||||||
|
handleConnect={handleConnect}
|
||||||
|
callback={callback}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{status === 'password' && (
|
||||||
|
<PasswordInput
|
||||||
|
isPass={password}
|
||||||
|
setPass={setPassword}
|
||||||
|
callback={handleSave}
|
||||||
|
loading={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
58
src/components/auth/views/BTCLogin.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
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: ({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
host?: string;
|
||||||
|
admin?: string;
|
||||||
|
viewOnly?: string;
|
||||||
|
cert?: string;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BTCLoginForm = ({ handleSet }: AuthProps) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [json, setJson] = useState('');
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
try {
|
||||||
|
JSON.parse(json);
|
||||||
|
const { cert, admin, viewOnly, host } = getConfigLnd(json);
|
||||||
|
handleSet({ name, host, admin, viewOnly, cert });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Invalid JSON');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canConnect = json !== '' && checked;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Line>
|
||||||
|
<StyledTitle>Name:</StyledTitle>
|
||||||
|
<Input onChange={e => setName(e.target.value)} />
|
||||||
|
</Line>
|
||||||
|
<Line>
|
||||||
|
<StyledTitle>BTCPayServer Connect JSON:</StyledTitle>
|
||||||
|
<Input onChange={e => setJson(e.target.value)} />
|
||||||
|
</Line>
|
||||||
|
<RiskCheckboxAndConfirm
|
||||||
|
disabled={!canConnect}
|
||||||
|
handleClick={handleClick}
|
||||||
|
checked={checked}
|
||||||
|
onChange={setChecked}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
55
src/components/auth/views/Checkboxes.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
60
src/components/auth/views/ConnectLogin.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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: ({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
host?: string;
|
||||||
|
admin?: string;
|
||||||
|
viewOnly?: string;
|
||||||
|
cert?: string;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectLoginForm = ({ handleSet }: AuthProps) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const { cert, macaroon, socket } = getAuthLnd(url);
|
||||||
|
const base64Cert = getBase64CertfromDerFormat(cert) || '';
|
||||||
|
|
||||||
|
handleSet({
|
||||||
|
name,
|
||||||
|
host: socket,
|
||||||
|
admin: macaroon,
|
||||||
|
cert: base64Cert,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const canConnect = url !== '' && checked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Line>
|
||||||
|
<StyledTitle>Name:</StyledTitle>
|
||||||
|
<Input onChange={e => setName(e.target.value)} />
|
||||||
|
</Line>
|
||||||
|
<Line>
|
||||||
|
<StyledTitle>LND Connect Url:</StyledTitle>
|
||||||
|
<Input onChange={e => setUrl(e.target.value)} />
|
||||||
|
</Line>
|
||||||
|
<RiskCheckboxAndConfirm
|
||||||
|
disabled={!canConnect}
|
||||||
|
handleClick={handleClick}
|
||||||
|
checked={checked}
|
||||||
|
onChange={setChecked}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
110
src/components/auth/views/NormalLogin.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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: ({
|
||||||
|
name,
|
||||||
|
host,
|
||||||
|
admin,
|
||||||
|
viewOnly,
|
||||||
|
cert,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
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 [name, setName] = useState('');
|
||||||
|
const [host, setHost] = useState('');
|
||||||
|
const [admin, setAdmin] = useState('');
|
||||||
|
const [viewOnly, setRead] = useState('');
|
||||||
|
const [cert, setCert] = useState('');
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
handleSet({ name, host, admin, viewOnly, cert });
|
||||||
|
};
|
||||||
|
|
||||||
|
const canConnect =
|
||||||
|
name !== '' &&
|
||||||
|
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>Name:</StyledTitle>
|
||||||
|
<Input
|
||||||
|
placeholder={'Name for this node (e.g.: My Awesome Node)'}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Line>
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
47
src/components/auth/views/Password.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
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 = 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
163
src/components/auth/views/QRLogin.tsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
27
src/components/bitcoinInfo/BitcoinFees.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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;
|
||||||
|
};
|
29
src/components/bitcoinInfo/BitcoinPrice.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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;
|
||||||
|
};
|
32
src/components/burgerMenu/BurgerMenu.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
31
src/components/buttons/colorButton/ColorButton.stories.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
159
src/components/buttons/colorButton/ColorButton.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
51
src/components/buttons/multiButton/MultiButton.stories.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
85
src/components/buttons/multiButton/MultiButton.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
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>;
|
||||||
|
};
|
115
src/components/buttons/secureButton/LoginModal.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
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 { getAuthString, saveSessionAuth } from '../../../utils/auth';
|
||||||
|
import { useSettings } from '../../../context/SettingsContext';
|
||||||
|
import { textColorMap } from '../../../styles/Themes';
|
||||||
|
import { ColorButton } from '../colorButton/ColorButton';
|
||||||
|
import { Input } from '../../input/Input';
|
||||||
|
|
||||||
|
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 { 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 = getAuthString(host, 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 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
64
src/components/buttons/secureButton/SecureButton.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import Modal from '../../modal/ReactModal';
|
||||||
|
import { LoginModal } from './LoginModal';
|
||||||
|
import { useAccount } from '../../../context/AccountContext';
|
||||||
|
import { getAuthString } from '../../../utils/auth';
|
||||||
|
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 = getAuthString(host, 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
16
src/components/checkbox/Checkbox.stories.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
58
src/components/checkbox/Checkbox.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|