mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-03-12 10:30:21 +01:00
Feat/v0.2.0 (#19)
* chore: bump version * style: password field changes * Feat/lerna integration (#17) * chore: move into package folder * chore: initial lerna setup * chore: yarn lock cleanup * chore: package organization * chore: yarn lock update * chore: update packages * chore: wip lerna integration * chore: downgrade date-fns * chore: lerna cleanup * chore: add env file * docs: update README * chore: organize configs * Feat/hodlhodl integration (#18) * feat: hodl integration start * chore: add pagination and more * chore: change filter flow * chore: change offer card ui * chore: more offer card changes * chore: small refactors and stylings * v0.2.0 * docs: update README
This commit is contained in:
parent
215b3355fe
commit
22196e8468
76 changed files with 6372 additions and 7841 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
66
README.md
66
README.md
|
@ -1,7 +1,7 @@
|
||||||
# **ThunderHub - Lightning Node Manager**
|
# **ThunderHub - Lightning Node Manager**
|
||||||
|
|
||||||

|

|
||||||
[](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [](https://snyk.io/test/github/apotdevin/thunderhub) [](https://snyk.io/test/github/apotdevin/thunderhub)
|
[](https://github.com/DAVFoundation/captain-n3m0/blob/master/LICENSE) [](https://snyk.io/test/github/apotdevin/thunderhub) [](https://snyk.io/test/github/apotdevin/thunderhub) [](https://lerna.js.org/)
|
||||||
|
|
||||||
## Table Of Contents
|
## Table Of Contents
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
|
|
||||||
|
The repository consists of two packages (client and server) and is maintained with LernaJS and Yarn Workspaces.
|
||||||
|
|
||||||
#### Client
|
#### Client
|
||||||
|
|
||||||
[](https://snyk.io/test/github/apotdevin/thunderhub)
|
[](https://snyk.io/test/github/apotdevin/thunderhub)
|
||||||
|
@ -48,6 +50,7 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
||||||
- View all transactions.
|
- View all transactions.
|
||||||
- View all forwarded payments.
|
- View all forwarded payments.
|
||||||
- View all chain transactions.
|
- View all chain transactions.
|
||||||
|
- View all unspent UTXOS.
|
||||||
|
|
||||||
### Management
|
### Management
|
||||||
|
|
||||||
|
@ -58,6 +61,7 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
||||||
- Balance your channels through circular payments. ([Check out the Tutorial](https://medium.com/coinmonks/lightning-network-channel-balancing-with-thunderhub-972b41bf9243))
|
- Balance your channels through circular payments. ([Check out the Tutorial](https://medium.com/coinmonks/lightning-network-channel-balancing-with-thunderhub-972b41bf9243))
|
||||||
- Update your all your channels fees or individual ones.
|
- Update your all your channels fees or individual ones.
|
||||||
- Backup, verify and recover all your channels.
|
- Backup, verify and recover all your channels.
|
||||||
|
- Sign and verify messages.
|
||||||
|
|
||||||
### Visual
|
### Visual
|
||||||
|
|
||||||
|
@ -69,13 +73,18 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
||||||
|
|
||||||
- Many ways to connect to your node: **HEX/Base64 strings**, **LNDConnect Url**, **BTCPayServer Info** or **QR codes**.
|
- Many ways to connect to your node: **HEX/Base64 strings**, **LNDConnect Url**, **BTCPayServer Info** or **QR codes**.
|
||||||
- Have view-only and/or admin accounts.
|
- Have view-only and/or admin accounts.
|
||||||
- Manage up to 10 different nodes.
|
- Manage however many accounts your browser storage can hold.
|
||||||
- Quickly sync your accounts between devices. No need to copy/paste macaroons and certificates.
|
- Quickly sync your accounts between devices. No need to copy/paste macaroons and certificates.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
- Docker images for easier deployment (WIP)
|
||||||
|
|
||||||
### Future Features
|
### Future Features
|
||||||
|
|
||||||
- Loop In and Out to provide liquidity or remove it from your channels.
|
- Loop In and Out to provide liquidity or remove it from your channels.
|
||||||
- Docker Image for easy deployments.
|
- Integration with HodlHodl
|
||||||
|
- Storefront interface
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -88,26 +97,19 @@ git clone https://github.com/apotdevin/thunderhub.git
|
||||||
### **Requirements**
|
### **Requirements**
|
||||||
|
|
||||||
- Node installed
|
- Node installed
|
||||||
- Yarn or NPM installed
|
- Yarn installed
|
||||||
|
|
||||||
After cloning the repository, go into both the `/client` and the `/server` folders, and run `yarn` or `npm install` in both of them to get all the necessary modules installed.
|
After cloning the repository run `yarn` to get all the necessary modules installed. Yarn workspaces will handle installing modules for both the client and the server.
|
||||||
|
|
||||||
### **ThunderHub - Server**
|
### **ThunderHub - Server**
|
||||||
|
|
||||||
|
To be able to use the HodlHodl integration create a `.env` file in the `/server` folder with `HODL_KEY='[YOUR API KEY]'` and replace `[YOUR API KEY]` with the one that HodlHodl provides you.
|
||||||
|
|
||||||
#### To get the server running use the following commands
|
#### To get the server running use the following commands
|
||||||
|
|
||||||
##### This must be done in the `/server` folder
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
//With yarn:
|
yarn server:prod
|
||||||
yarn build
|
yarn server:run
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
//With npm:
|
|
||||||
npm run build
|
|
||||||
npm run start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If the server starts succesfully, you should see `info [server.js]: Server ready at http://localhost:3001/` in the terminal
|
If the server starts succesfully, you should see `info [server.js]: Server ready at http://localhost:3001/` in the terminal
|
||||||
|
@ -119,51 +121,29 @@ If the server starts succesfully, you should see `info [server.js]: Server ready
|
||||||
##### This must be done in the `/client` folder
|
##### This must be done in the `/client` folder
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
//With yarn:
|
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
|
||||||
//With npm:
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
If the frontend starts succesfully, you should see `Compiled successfully! You can now view app in the browser.` in the terminal and a browser window should have opened in your browser.
|
If the frontend starts succesfully, you should see `Compiled successfully! You can now view app in the browser.` in the terminal and a browser window should have opened in your browser.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
If you want to develop on ThunderHub and want hot reloading when you do changes use the following commands
|
If you want to develop on ThunderHub and want hot reloading when you do changes, use the following commands:
|
||||||
|
|
||||||
### ThunderHub - Server
|
### ThunderHub - Server
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
//With yarn:
|
yarn server:dev
|
||||||
yarn build:dev
|
|
||||||
|
|
||||||
// In another terminal
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
//With npm:
|
|
||||||
npm run build:dev
|
|
||||||
|
|
||||||
// In another terminal
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### ThunderHub - Client
|
### ThunderHub - Client
|
||||||
|
|
||||||
Running the commands `yarn start` or `npm run start` works for development.
|
Running the commands `yarn start` in the `client` folder works for development.
|
||||||
|
|
||||||
|
#### Storybook
|
||||||
|
|
||||||
You can also get storybook running for quicker component development.
|
You can also get storybook running for quicker component development.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
//With yarn:
|
|
||||||
yarn storybook
|
yarn storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
|
||||||
//With npm:
|
|
||||||
npm run storybook
|
|
||||||
```
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ module.exports = {
|
||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
'@storybook/addon-viewport/register',
|
'@storybook/addon-viewport/register',
|
||||||
],
|
],
|
||||||
webpackFinal: async config => {
|
webpackFinal: async (config) => {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.(ts|tsx)$/,
|
test: /\.(ts|tsx)$/,
|
||||||
use: [
|
use: [
|
||||||
|
|
|
@ -17,7 +17,7 @@ const StyledBackground = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ThemeDecorator = storyFn => {
|
const ThemeDecorator = (storyFn) => {
|
||||||
const background = boolean('No Background', false);
|
const background = boolean('No Background', false);
|
||||||
const cardBackground = boolean('Card Background', true);
|
const cardBackground = boolean('Card Background', true);
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "thunderhub-client",
|
"name": "@thunderhub/client",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
@ -9,7 +9,8 @@
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"storybook": "start-storybook -p 9009 -s public",
|
"storybook": "start-storybook -p 9009 -s public",
|
||||||
"build-storybook": "build-storybook -s public",
|
"build-storybook": "build-storybook -s public",
|
||||||
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client"
|
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client",
|
||||||
|
"precommit": "pretty-quick --staged"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -21,14 +22,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/react-hooks": "^3.1.3",
|
"@apollo/react-hooks": "^3.1.3",
|
||||||
"@types/crypto-js": "^3.1.44",
|
"@types/crypto-js": "^3.1.44",
|
||||||
"@types/jest": "25.1.4",
|
"@types/jest": "25.1.5",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/lodash.merge": "^4.6.6",
|
"@types/lodash.merge": "^4.6.6",
|
||||||
"@types/lodash.sortby": "^4.7.6",
|
"@types/lodash.sortby": "^4.7.6",
|
||||||
"@types/node": "13.9.8",
|
"@types/node": "13.11.0",
|
||||||
"@types/numeral": "^0.0.26",
|
"@types/numeral": "^0.0.26",
|
||||||
"@types/qrcode.react": "^1.0.0",
|
"@types/qrcode.react": "^1.0.0",
|
||||||
"@types/react": "16.9.31",
|
"@types/react": "16.9.32",
|
||||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||||
"@types/react-dom": "16.9.6",
|
"@types/react-dom": "16.9.6",
|
||||||
"@types/react-modal": "^3.10.5",
|
"@types/react-modal": "^3.10.5",
|
||||||
|
@ -42,10 +43,7 @@
|
||||||
"@types/victory": "^33.1.4",
|
"@types/victory": "^33.1.4",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
"apollo-boost": "^0.4.4",
|
"apollo-boost": "^0.4.4",
|
||||||
"base64url": "^3.0.1",
|
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"date-fns": "^2.11.0",
|
|
||||||
"graphql": "^14.6.0",
|
|
||||||
"intersection-observer": "^0.7.0",
|
"intersection-observer": "^0.7.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
@ -53,6 +51,7 @@
|
||||||
"node-sass": "^4.13.0",
|
"node-sass": "^4.13.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"qrcode.react": "^1.0.0",
|
"qrcode.react": "^1.0.0",
|
||||||
|
"qs": "^6.9.3",
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.0",
|
||||||
"react-copy-to-clipboard": "^5.0.2",
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.0",
|
||||||
|
@ -83,9 +82,6 @@
|
||||||
"@storybook/preset-create-react-app": "^2.1.1",
|
"@storybook/preset-create-react-app": "^2.1.1",
|
||||||
"@storybook/react": "^5.3.18",
|
"@storybook/react": "^5.3.18",
|
||||||
"awesome-typescript-loader": "^5.2.1",
|
"awesome-typescript-loader": "^5.2.1",
|
||||||
"husky": "^4.2.3",
|
|
||||||
"prettier": "2.0.2",
|
|
||||||
"pretty-quick": "^2.0.1",
|
|
||||||
"react-docgen-typescript-loader": "^3.7.2"
|
"react-docgen-typescript-loader": "^3.7.2"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -102,10 +98,5 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "pretty-quick --staged"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
client/src/assets/icons/credit-card.svg
Normal file
1
client/src/assets/icons/credit-card.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-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg>
|
After Width: | Height: | Size: 329 B |
1
client/src/assets/icons/half-star.svg
Normal file
1
client/src/assets/icons/half-star.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-half-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon><polygon fill="currentColor" points="12 2 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
After Width: | Height: | Size: 438 B |
1
client/src/assets/icons/star.svg
Normal file
1
client/src/assets/icons/star.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-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
|
After Width: | Height: | Size: 339 B |
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { useSpring, animated } from 'react-spring';
|
import { useSpring, animated } from 'react-spring';
|
||||||
import { getValue } from '../../helpers/Helpers';
|
import { getValue } from '../../helpers/Helpers';
|
||||||
import { useSettings } from '../../context/SettingsContext';
|
import { useSettings } from '../../context/SettingsContext';
|
||||||
import { usePriceState } from 'context/PriceContext';
|
import { usePriceState } from '../../context/PriceContext';
|
||||||
|
|
||||||
type PriceProps = {
|
type PriceProps = {
|
||||||
price: number;
|
price: number;
|
||||||
|
@ -43,7 +43,7 @@ export const AnimatedNumber = ({ amount }: AnimatedProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.div>
|
<animated.div>
|
||||||
{value.interpolate(amount => getValue({ amount, ...priceProps }))}
|
{value.interpolate((amount) => getValue({ amount, ...priceProps }))}
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const PasswordInput = ({
|
||||||
<SubTitle>Please Input a Password</SubTitle>
|
<SubTitle>Please Input a Password</SubTitle>
|
||||||
<Line>
|
<Line>
|
||||||
<Sub4Title>Password:</Sub4Title>
|
<Sub4Title>Password:</Sub4Title>
|
||||||
<Input onChange={e => setPass(e.target.value)} />
|
<Input onChange={(e) => setPass(e.target.value)} />
|
||||||
</Line>
|
</Line>
|
||||||
<Line>
|
<Line>
|
||||||
<Sub4Title>Strength:</Sub4Title>
|
<Sub4Title>Strength:</Sub4Title>
|
||||||
|
|
|
@ -12,9 +12,10 @@ import styled from 'styled-components';
|
||||||
import { useAccount } from '../../../context/AccountContext';
|
import { useAccount } from '../../../context/AccountContext';
|
||||||
import { saveSessionAuth } from '../../../utils/auth';
|
import { saveSessionAuth } from '../../../utils/auth';
|
||||||
import { useSettings } from '../../../context/SettingsContext';
|
import { useSettings } from '../../../context/SettingsContext';
|
||||||
import { textColorMap } from '../../../styles/Themes';
|
import { textColorMap, mediaDimensions } from '../../../styles/Themes';
|
||||||
import { ColorButton } from '../colorButton/ColorButton';
|
import { ColorButton } from '../colorButton/ColorButton';
|
||||||
import { Input } from '../../input/Input';
|
import { Input } from '../../input/Input';
|
||||||
|
import { useSize } from 'hooks/UseSize';
|
||||||
|
|
||||||
const RadioText = styled.div`
|
const RadioText = styled.div`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
@ -42,7 +43,9 @@ export const LoginModal = ({
|
||||||
callback,
|
callback,
|
||||||
variables,
|
variables,
|
||||||
}: LoginProps) => {
|
}: LoginProps) => {
|
||||||
|
const { width } = useSize();
|
||||||
const { theme } = useSettings();
|
const { theme } = useSettings();
|
||||||
|
|
||||||
const [pass, setPass] = useState<string>('');
|
const [pass, setPass] = useState<string>('');
|
||||||
const [storeSession, setStoreSession] = useState<boolean>(false);
|
const [storeSession, setStoreSession] = useState<boolean>(false);
|
||||||
const { host, cert, refreshAccount } = useAccount();
|
const { host, cert, refreshAccount } = useAccount();
|
||||||
|
@ -83,7 +86,13 @@ export const LoginModal = ({
|
||||||
<SubTitle>Unlock your Account</SubTitle>
|
<SubTitle>Unlock your Account</SubTitle>
|
||||||
<ResponsiveLine>
|
<ResponsiveLine>
|
||||||
<Sub4Title>Password:</Sub4Title>
|
<Sub4Title>Password:</Sub4Title>
|
||||||
<Input onChange={(e) => setPass(e.target.value)} />
|
<Input
|
||||||
|
withMargin={
|
||||||
|
width <= mediaDimensions.mobile ? '0' : '0 0 0 16px'
|
||||||
|
}
|
||||||
|
type={'password'}
|
||||||
|
onChange={(e) => setPass(e.target.value)}
|
||||||
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<ResponsiveLine>
|
<ResponsiveLine>
|
||||||
<NoWrapTitle>Don't ask me again this session:</NoWrapTitle>
|
<NoWrapTitle>Don't ask me again this session:</NoWrapTitle>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SmallLink, DarkSubTitle, OverflowText } from './Styled';
|
import { SmallLink, DarkSubTitle, OverflowText, SingleLine } from './Styled';
|
||||||
import { StatusDot, DetailLine } from '../../views/channels/Channels.style';
|
import { StatusDot, DetailLine } from '../../views/channels/Channels.style';
|
||||||
import { formatDistanceStrict, format } from 'date-fns';
|
import { formatDistanceStrict, format } from 'date-fns';
|
||||||
|
import { XSvg } from './Icons';
|
||||||
|
|
||||||
export const getTransactionLink = (transaction: string) => {
|
export const getTransactionLink = (transaction: string) => {
|
||||||
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
|
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
|
||||||
|
@ -51,12 +52,23 @@ export const renderLine = (
|
||||||
title: string,
|
title: string,
|
||||||
content: any,
|
content: any,
|
||||||
key?: string | number,
|
key?: string | number,
|
||||||
|
deleteCallback?: () => void,
|
||||||
) => {
|
) => {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
return (
|
return (
|
||||||
<DetailLine key={key}>
|
<DetailLine key={key}>
|
||||||
<DarkSubTitle>{title}</DarkSubTitle>
|
<DarkSubTitle>{title}</DarkSubTitle>
|
||||||
|
<SingleLine>
|
||||||
<OverflowText>{content}</OverflowText>
|
<OverflowText>{content}</OverflowText>
|
||||||
|
{deleteCallback && (
|
||||||
|
<div
|
||||||
|
style={{ margin: '0 0 -4px 4px' }}
|
||||||
|
onClick={deleteCallback}
|
||||||
|
>
|
||||||
|
<XSvg />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SingleLine>
|
||||||
</DetailLine>
|
</DetailLine>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,6 +47,9 @@ import { ReactComponent as Mail } from '../../assets/icons/mail.svg';
|
||||||
import { ReactComponent as Github } from '../../assets/icons/github.svg';
|
import { ReactComponent as Github } from '../../assets/icons/github.svg';
|
||||||
import { ReactComponent as Repeat } from '../../assets/icons/repeat.svg';
|
import { ReactComponent as Repeat } from '../../assets/icons/repeat.svg';
|
||||||
import { ReactComponent as CheckIcon } from '../../assets/icons/check.svg';
|
import { ReactComponent as CheckIcon } from '../../assets/icons/check.svg';
|
||||||
|
import { ReactComponent as StarIcon } from '../../assets/icons/star.svg';
|
||||||
|
import { ReactComponent as HalfStarIcon } from '../../assets/icons/half-star.svg';
|
||||||
|
import { ReactComponent as CreditCardIcon } from '../../assets/icons/credit-card.svg';
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
color?: string;
|
color?: string;
|
||||||
|
@ -116,3 +119,6 @@ export const MailIcon = styleIcon(Mail);
|
||||||
export const GithubIcon = styleIcon(Github);
|
export const GithubIcon = styleIcon(Github);
|
||||||
export const RepeatIcon = styleIcon(Repeat);
|
export const RepeatIcon = styleIcon(Repeat);
|
||||||
export const Check = styleIcon(CheckIcon);
|
export const Check = styleIcon(CheckIcon);
|
||||||
|
export const Star = styleIcon(StarIcon);
|
||||||
|
export const HalfStar = styleIcon(HalfStarIcon);
|
||||||
|
export const CreditCard = styleIcon(CreditCardIcon);
|
||||||
|
|
|
@ -56,10 +56,11 @@ export const Separation = styled.div`
|
||||||
interface SubCardProps {
|
interface SubCardProps {
|
||||||
color?: string;
|
color?: string;
|
||||||
padding?: string;
|
padding?: string;
|
||||||
|
withMargin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubCard = styled.div`
|
export const SubCard = styled.div`
|
||||||
margin-bottom: 10px;
|
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
|
||||||
padding: ${({ padding }) => (padding ? padding : '16px')};
|
padding: ${({ padding }) => (padding ? padding : '16px')};
|
||||||
background: ${subCardColor};
|
background: ${subCardColor};
|
||||||
border: 1px solid ${cardBorderColor};
|
border: 1px solid ${cardBorderColor};
|
||||||
|
|
|
@ -76,7 +76,7 @@ export const Input = ({
|
||||||
value={value}
|
value={value}
|
||||||
color={color}
|
color={color}
|
||||||
withMargin={withMargin}
|
withMargin={withMargin}
|
||||||
onChange={e => onChange(e)}
|
onChange={(e) => onChange(e)}
|
||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
inputWidth={width}
|
inputWidth={width}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
|
|
|
@ -8,12 +8,18 @@ interface StyledProps {
|
||||||
fontColor?: string | ThemeSet;
|
fontColor?: string | ThemeSet;
|
||||||
underline?: string | ThemeSet;
|
underline?: string | ThemeSet;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styledCss = css`
|
const styledCss = css`
|
||||||
color: ${({ fontColor, inheritColor }: StyledProps) =>
|
color: ${({ fontColor, inheritColor }: StyledProps) =>
|
||||||
inheritColor ? 'inherit' : fontColor ?? textColor};
|
inheritColor ? 'inherit' : fontColor ?? textColor};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
${({ fullWidth }: StyledProps) =>
|
||||||
|
fullWidth &&
|
||||||
|
css`
|
||||||
|
width: 100%;
|
||||||
|
`};
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
|
@ -27,9 +33,11 @@ const styledCss = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLink = styled(({ inheritColor, fontColor, underline, ...rest }) => (
|
const StyledLink = styled(
|
||||||
|
({ inheritColor, fontColor, underline, fullWidth, ...rest }) => (
|
||||||
<RouterLink {...rest} />
|
<RouterLink {...rest} />
|
||||||
))(() => styledCss);
|
),
|
||||||
|
)(() => styledCss);
|
||||||
|
|
||||||
const StyledALink = styled.a`
|
const StyledALink = styled.a`
|
||||||
${styledCss}
|
${styledCss}
|
||||||
|
@ -42,6 +50,7 @@ interface LinkProps {
|
||||||
color?: string | ThemeSet;
|
color?: string | ThemeSet;
|
||||||
underline?: string | ThemeSet;
|
underline?: string | ThemeSet;
|
||||||
inheritColor?: boolean;
|
inheritColor?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Link = ({
|
export const Link = ({
|
||||||
|
@ -51,8 +60,9 @@ export const Link = ({
|
||||||
color,
|
color,
|
||||||
underline,
|
underline,
|
||||||
inheritColor,
|
inheritColor,
|
||||||
|
fullWidth,
|
||||||
}: LinkProps) => {
|
}: LinkProps) => {
|
||||||
const props = { fontColor: color, underline, inheritColor };
|
const props = { fontColor: color, underline, inheritColor, fullWidth };
|
||||||
|
|
||||||
if (!to && !href) return null;
|
if (!to && !href) return null;
|
||||||
|
|
||||||
|
|
20
client/src/components/rating/Rating.stories.tsx
Normal file
20
client/src/components/rating/Rating.stories.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { number } from '@storybook/addon-knobs';
|
||||||
|
import { Rating } from './Rating';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Ratings',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => {
|
||||||
|
const options = {
|
||||||
|
range: true,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rating = number('Rating', 5, options);
|
||||||
|
|
||||||
|
return <Rating rating={rating / 10} />;
|
||||||
|
};
|
62
client/src/components/rating/Rating.tsx
Normal file
62
client/src/components/rating/Rating.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Star, HalfStar } from '../../components/generic/Icons';
|
||||||
|
import { themeColors } from '../../styles/Themes';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledStar = styled(Star)`
|
||||||
|
margin-bottom: -1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledHalfStar = styled(HalfStar)`
|
||||||
|
margin-bottom: -1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRatings = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface RatingProps {
|
||||||
|
rating: number | null;
|
||||||
|
size?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Rating = ({
|
||||||
|
rating,
|
||||||
|
size = '14px',
|
||||||
|
color = themeColors.blue3,
|
||||||
|
}: RatingProps) => {
|
||||||
|
if (!rating) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const correctRating = Math.min(Math.max(Math.round(rating * 10), 0), 10);
|
||||||
|
|
||||||
|
const amount = (correctRating - (correctRating % 2)) / 2;
|
||||||
|
const hasHalf = correctRating % 2 > 0 ? true : false;
|
||||||
|
|
||||||
|
let stars = [];
|
||||||
|
|
||||||
|
const starConfig = {
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (i < amount) {
|
||||||
|
stars.push(
|
||||||
|
<StyledStar
|
||||||
|
key={i}
|
||||||
|
{...starConfig}
|
||||||
|
fillcolor={themeColors.blue3}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
} else if (hasHalf && i === amount) {
|
||||||
|
stars.push(<StyledHalfStar key={i} {...starConfig} />);
|
||||||
|
} else {
|
||||||
|
stars.push(<StyledStar key={i} {...starConfig} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StyledRatings>{stars.map((star) => star)}</StyledRatings>;
|
||||||
|
};
|
71
client/src/graphql/hodlhodl/query.ts
Normal file
71
client/src/graphql/hodlhodl/query.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
export const GET_HODL_COUNTRIES = gql`
|
||||||
|
query GetCountries {
|
||||||
|
getCountries {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
native_name
|
||||||
|
currency_code
|
||||||
|
currency_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_HODL_CURRENCIES = gql`
|
||||||
|
query GetCurrencies {
|
||||||
|
getCurrencies {
|
||||||
|
code
|
||||||
|
name
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_HODL_OFFERS = gql`
|
||||||
|
query GetOffers($filter: String) {
|
||||||
|
getOffers(filter: $filter) {
|
||||||
|
id
|
||||||
|
asset_code
|
||||||
|
country
|
||||||
|
country_code
|
||||||
|
working_now
|
||||||
|
side
|
||||||
|
title
|
||||||
|
description
|
||||||
|
currency_code
|
||||||
|
price
|
||||||
|
min_amount
|
||||||
|
max_amount
|
||||||
|
first_trade_limit
|
||||||
|
fee {
|
||||||
|
author_fee_rate
|
||||||
|
}
|
||||||
|
balance
|
||||||
|
payment_window_minutes
|
||||||
|
confirmations
|
||||||
|
payment_method_instructions {
|
||||||
|
id
|
||||||
|
version
|
||||||
|
payment_method_id
|
||||||
|
payment_method_type
|
||||||
|
payment_method_name
|
||||||
|
}
|
||||||
|
trader {
|
||||||
|
login
|
||||||
|
online_status
|
||||||
|
rating
|
||||||
|
trades_count
|
||||||
|
url
|
||||||
|
verified
|
||||||
|
verified_by
|
||||||
|
strong_hodler
|
||||||
|
country
|
||||||
|
country_code
|
||||||
|
average_payment_time_minutes
|
||||||
|
average_release_time_minutes
|
||||||
|
days_since_last_trade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
|
@ -23,6 +23,7 @@ import { BalanceView } from 'views/balance/Balance';
|
||||||
import { PeersList } from 'views/peers/PeersList';
|
import { PeersList } from 'views/peers/PeersList';
|
||||||
import { ToolsView } from 'views/tools';
|
import { ToolsView } from 'views/tools';
|
||||||
import { ChainView } from 'views/chain/ChainView';
|
import { ChainView } from 'views/chain/ChainView';
|
||||||
|
import { TraderView } from 'views/trader/TraderView';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -79,6 +80,7 @@ const Content = () => {
|
||||||
/>
|
/>
|
||||||
<Route path="/settings" render={() => getGrid(SettingsView)} />
|
<Route path="/settings" render={() => getGrid(SettingsView)} />
|
||||||
<Route path="/fees" render={() => getGrid(FeesView)} />
|
<Route path="/fees" render={() => getGrid(FeesView)} />
|
||||||
|
<Route path="/trading" render={() => getGrid(TraderView)} />
|
||||||
<Route path="/terms" render={() => <TermsView />} />
|
<Route path="/terms" render={() => <TermsView />} />
|
||||||
<Route path="/privacy" render={() => <PrivacyView />} />
|
<Route path="/privacy" render={() => <PrivacyView />} />
|
||||||
<Route path="/faq" render={() => <FaqView />} />
|
<Route path="/faq" render={() => <FaqView />} />
|
||||||
|
|
|
@ -85,7 +85,7 @@ export const Header = () => {
|
||||||
const renderLoggedIn = () => {
|
const renderLoggedIn = () => {
|
||||||
if (width <= mediaDimensions.mobile) {
|
if (width <= mediaDimensions.mobile) {
|
||||||
return (
|
return (
|
||||||
<IconWrapper onClick={() => setOpen(prev => !prev)}>
|
<IconWrapper onClick={() => setOpen((prev) => !prev)}>
|
||||||
{transitions.map(({ item, key, props }) =>
|
{transitions.map(({ item, key, props }) =>
|
||||||
item ? (
|
item ? (
|
||||||
<AnimatedClose
|
<AnimatedClose
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
RepeatIcon,
|
RepeatIcon,
|
||||||
Users,
|
Users,
|
||||||
|
CreditCard,
|
||||||
} from '../../components/generic/Icons';
|
} from '../../components/generic/Icons';
|
||||||
import { useSettings } from '../../context/SettingsContext';
|
import { useSettings } from '../../context/SettingsContext';
|
||||||
import { useConnectionState } from 'context/ConnectionContext';
|
import { useConnectionState } from 'context/ConnectionContext';
|
||||||
|
@ -59,6 +60,7 @@ const ButtonSection = styled.div`
|
||||||
|
|
||||||
const NavSeparation = styled.div`
|
const NavSeparation = styled.div`
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface NavProps {
|
interface NavProps {
|
||||||
|
@ -124,6 +126,7 @@ const CHAIN_TRANS = '/chainTransactions';
|
||||||
const TOOLS = '/tools';
|
const TOOLS = '/tools';
|
||||||
const SETTINGS = '/settings';
|
const SETTINGS = '/settings';
|
||||||
const FEES = '/fees';
|
const FEES = '/fees';
|
||||||
|
const TRADER = '/trading';
|
||||||
|
|
||||||
interface NavigationProps {
|
interface NavigationProps {
|
||||||
isBurger?: boolean;
|
isBurger?: boolean;
|
||||||
|
@ -169,6 +172,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
||||||
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
|
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
|
||||||
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
|
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
|
||||||
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
|
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
|
||||||
|
{renderNavButton('P2P Trading', TRADER, CreditCard, sidebar)}
|
||||||
{renderNavButton('Settings', SETTINGS, Settings, sidebar)}
|
{renderNavButton('Settings', SETTINGS, Settings, sidebar)}
|
||||||
</ButtonSection>
|
</ButtonSection>
|
||||||
);
|
);
|
||||||
|
@ -184,6 +188,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
||||||
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
|
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
|
||||||
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
|
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
|
||||||
{renderBurgerNav('Tools', TOOLS, Shield)}
|
{renderBurgerNav('Tools', TOOLS, Shield)}
|
||||||
|
{renderBurgerNav('Trading', TRADER, CreditCard)}
|
||||||
{renderBurgerNav('Settings', SETTINGS, Settings)}
|
{renderBurgerNav('Settings', SETTINGS, Settings)}
|
||||||
</BurgerRow>
|
</BurgerRow>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,8 +16,8 @@ const isLocalhost = Boolean(
|
||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === '[::1]' ||
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
window.location.hostname.match(
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
|
@ -30,7 +30,7 @@ export function register(config?: Config) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(
|
const publicUrl = new URL(
|
||||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
||||||
window.location.href
|
window.location.href,
|
||||||
);
|
);
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
@ -51,7 +51,7 @@ export function register(config?: Config) {
|
||||||
navigator.serviceWorker.ready.then(() => {
|
navigator.serviceWorker.ready.then(() => {
|
||||||
console.log(
|
console.log(
|
||||||
'This web app is being served cache-first by a service ' +
|
'This web app is being served cache-first by a service ' +
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
'worker. To learn more, visit https://bit.ly/CRA-PWA',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -65,7 +65,7 @@ export function register(config?: Config) {
|
||||||
function registerValidSW(swUrl: string, config?: Config) {
|
function registerValidSW(swUrl: string, config?: Config) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing;
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
|
@ -79,7 +79,7 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||||
// content until all client tabs are closed.
|
// content until all client tabs are closed.
|
||||||
console.log(
|
console.log(
|
||||||
'New content is available and will be used when all ' +
|
'New content is available and will be used when all ' +
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
|
@ -101,7 +101,7 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error('Error during service worker registration:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -109,15 +109,16 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl)
|
fetch(swUrl)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
if (
|
if (
|
||||||
response.status === 404 ||
|
response.status === 404 ||
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
(contentType != null &&
|
||||||
|
contentType.indexOf('javascript') === -1)
|
||||||
) {
|
) {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
@ -129,14 +130,14 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log(
|
console.log(
|
||||||
'No internet connection found. App is running in offline mode.'
|
'No internet connection found. App is running in offline mode.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
|
||||||
import { ApolloError } from 'apollo-boost';
|
import { ApolloError } from 'apollo-boost';
|
||||||
|
|
||||||
export const getErrorContent = (error: ApolloError): ReactNode => {
|
export const getErrorContent = (error: ApolloError): ReactNode => {
|
||||||
const errors = error.graphQLErrors.map(x => x.message);
|
const errors = error.graphQLErrors.map((x) => x.message);
|
||||||
|
|
||||||
const renderMessage = errors.map((error, i) => {
|
const renderMessage = errors.map((error, i) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const BalanceRoute = ({
|
||||||
}: BalancedRouteProps) => {
|
}: BalancedRouteProps) => {
|
||||||
const [getRoute, { loading, data, called }] = useLazyQuery(GET_ROUTES, {
|
const [getRoute, { loading, data, called }] = useLazyQuery(GET_ROUTES, {
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
onError: error => {
|
onError: (error) => {
|
||||||
callback();
|
callback();
|
||||||
toast.error(getErrorContent(error));
|
toast.error(getErrorContent(error));
|
||||||
},
|
},
|
||||||
|
@ -52,7 +52,7 @@ export const BalanceRoute = ({
|
||||||
incoming && outgoing && amount && data && data.getRoutes && blocked;
|
incoming && outgoing && amount && data && data.getRoutes && blocked;
|
||||||
|
|
||||||
const [payRoute, { loading: loadingP }] = useMutation(PAY_VIA_ROUTE, {
|
const [payRoute, { loading: loadingP }] = useMutation(PAY_VIA_ROUTE, {
|
||||||
onError: error => {
|
onError: (error) => {
|
||||||
callback();
|
callback();
|
||||||
toast.error(getErrorContent(error));
|
toast.error(getErrorContent(error));
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,8 +38,9 @@ export const SessionLogin = () => {
|
||||||
<SingleLine>
|
<SingleLine>
|
||||||
<Sub4Title>Password:</Sub4Title>
|
<Sub4Title>Password:</Sub4Title>
|
||||||
<Input
|
<Input
|
||||||
|
type={'password'}
|
||||||
withMargin={'0 0 0 16px'}
|
withMargin={'0 0 0 16px'}
|
||||||
onChange={e => setPass(e.target.value)}
|
onChange={(e) => setPass(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</SingleLine>
|
</SingleLine>
|
||||||
{pass !== '' && (
|
{pass !== '' && (
|
||||||
|
|
|
@ -48,8 +48,8 @@ export const FeeCard = ({
|
||||||
} = channelInfo;
|
} = channelInfo;
|
||||||
|
|
||||||
const [updateFees] = useMutation(UPDATE_FEES, {
|
const [updateFees] = useMutation(UPDATE_FEES, {
|
||||||
onError: error => toast.error(getErrorContent(error)),
|
onError: (error) => toast.error(getErrorContent(error)),
|
||||||
onCompleted: data => {
|
onCompleted: (data) => {
|
||||||
setIndexOpen(0);
|
setIndexOpen(0);
|
||||||
data.updateFees
|
data.updateFees
|
||||||
? toast.success('Channel fees updated')
|
? toast.success('Channel fees updated')
|
||||||
|
@ -82,7 +82,9 @@ export const FeeCard = ({
|
||||||
placeholder={'Sats'}
|
placeholder={'Sats'}
|
||||||
color={textColorMap[theme]}
|
color={textColorMap[theme]}
|
||||||
type={textColorMap[theme]}
|
type={textColorMap[theme]}
|
||||||
onChange={e => setBaseFee(parseInt(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setBaseFee(parseInt(e.target.value))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<ResponsiveLine>
|
<ResponsiveLine>
|
||||||
|
@ -93,7 +95,9 @@ export const FeeCard = ({
|
||||||
placeholder={'Sats/Million'}
|
placeholder={'Sats/Million'}
|
||||||
color={textColorMap[theme]}
|
color={textColorMap[theme]}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
onChange={e => setFeeRate(parseInt(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setFeeRate(parseInt(e.target.value))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<SecureButton
|
<SecureButton
|
||||||
|
|
|
@ -56,7 +56,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
|
||||||
const [request, setRequest] = useState('');
|
const [request, setRequest] = useState('');
|
||||||
|
|
||||||
const [createInvoice, { data, loading }] = useMutation(CREATE_INVOICE, {
|
const [createInvoice, { data, loading }] = useMutation(CREATE_INVOICE, {
|
||||||
onError: error => toast.error(getErrorContent(error)),
|
onError: (error) => toast.error(getErrorContent(error)),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -104,7 +104,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
|
||||||
}
|
}
|
||||||
color={color}
|
color={color}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
onChange={e => setAmount(parseInt(e.target.value))}
|
onChange={(e) => setAmount(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
<SecureButton
|
<SecureButton
|
||||||
callback={createInvoice}
|
callback={createInvoice}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const ReceiveOnChainCard = () => {
|
||||||
const [received, setReceived] = useState(false);
|
const [received, setReceived] = useState(false);
|
||||||
|
|
||||||
const [createAddress, { data, loading }] = useMutation(CREATE_ADDRESS, {
|
const [createAddress, { data, loading }] = useMutation(CREATE_ADDRESS, {
|
||||||
onError: error => toast.error(getErrorContent(error)),
|
onError: (error) => toast.error(getErrorContent(error)),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
||||||
const { fast, halfHour, hour } = useBitcoinState();
|
const { fast, halfHour, hour } = useBitcoinState();
|
||||||
|
|
||||||
const [payAddress, { loading }] = useMutation(PAY_ADDRESS, {
|
const [payAddress, { loading }] = useMutation(PAY_ADDRESS, {
|
||||||
onError: error => toast.error(getErrorContent(error)),
|
onError: (error) => toast.error(getErrorContent(error)),
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Payment Sent!');
|
toast.success('Payment Sent!');
|
||||||
setOpen();
|
setOpen();
|
||||||
|
@ -113,7 +113,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
||||||
withMargin={
|
withMargin={
|
||||||
width <= mediaDimensions.mobile ? '' : '0 0 0 24px'
|
width <= mediaDimensions.mobile ? '' : '0 0 0 24px'
|
||||||
}
|
}
|
||||||
onChange={e => setAddress(e.target.value)}
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<Separation />
|
<Separation />
|
||||||
|
@ -140,7 +140,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
||||||
placeholder={'Sats'}
|
placeholder={'Sats'}
|
||||||
withMargin={'0 0 0 8px'}
|
withMargin={'0 0 0 8px'}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
onChange={e => setTokens(parseInt(e.target.value))}
|
onChange={(e) => setTokens(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</SingleLine>
|
</SingleLine>
|
||||||
)}
|
)}
|
||||||
|
@ -193,7 +193,9 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
||||||
}
|
}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
withMargin={'0 0 0 8px'}
|
withMargin={'0 0 0 8px'}
|
||||||
onChange={e => setAmount(parseInt(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setAmount(parseInt(e.target.value))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
||||||
const { fast, halfHour, hour } = useBitcoinState();
|
const { fast, halfHour, hour } = useBitcoinState();
|
||||||
|
|
||||||
const [openChannel] = useMutation(OPEN_CHANNEL, {
|
const [openChannel] = useMutation(OPEN_CHANNEL, {
|
||||||
onError: error => toast.error(getErrorContent(error)),
|
onError: (error) => toast.error(getErrorContent(error)),
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Channel Opened');
|
toast.success('Channel Opened');
|
||||||
setOpenCard('none');
|
setOpenCard('none');
|
||||||
|
@ -82,7 +82,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
||||||
withMargin={
|
withMargin={
|
||||||
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
||||||
}
|
}
|
||||||
onChange={e => setPublicKey(e.target.value)}
|
onChange={(e) => setPublicKey(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<ResponsiveLine>
|
<ResponsiveLine>
|
||||||
|
@ -101,7 +101,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
||||||
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
||||||
}
|
}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
onChange={e => setSize(parseInt(e.target.value))}
|
onChange={(e) => setSize(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</ResponsiveLine>
|
</ResponsiveLine>
|
||||||
<Separation />
|
<Separation />
|
||||||
|
@ -152,7 +152,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
||||||
placeholder={'Sats/Byte'}
|
placeholder={'Sats/Byte'}
|
||||||
color={color}
|
color={color}
|
||||||
type={'number'}
|
type={'number'}
|
||||||
onChange={e => setFee(parseInt(e.target.value))}
|
onChange={(e) => setFee(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
// </MultiButton>
|
// </MultiButton>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -96,7 +96,7 @@ Props) => {
|
||||||
grid: { stroke: chartGridColor[theme] },
|
grid: { stroke: chartGridColor[theme] },
|
||||||
axis: { stroke: 'transparent' },
|
axis: { stroke: 'transparent' },
|
||||||
}}
|
}}
|
||||||
tickFormat={a =>
|
tickFormat={(a) =>
|
||||||
isType === 'tokens' ? format({ amount: a }) : a
|
isType === 'tokens' ? format({ amount: a }) : a
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,10 +21,10 @@ export const getWaterfall = (
|
||||||
|
|
||||||
for (let i = initialPeriod; i <= lastPeriod; i++) {
|
for (let i = initialPeriod; i <= lastPeriod; i++) {
|
||||||
const currentInvoice = invoices.find(
|
const currentInvoice = invoices.find(
|
||||||
invoice => invoice.period === i,
|
(invoice) => invoice.period === i,
|
||||||
) ?? { period: undefined, amount: 0, tokens: 0 };
|
) ?? { period: undefined, amount: 0, tokens: 0 };
|
||||||
const currentPayment = payments.find(
|
const currentPayment = payments.find(
|
||||||
payment => payment.period === i,
|
(payment) => payment.period === i,
|
||||||
) ?? { period: undefined, amount: 0, tokens: 0 };
|
) ?? { period: undefined, amount: 0, tokens: 0 };
|
||||||
|
|
||||||
const amountChange = currentInvoice.amount - currentPayment.amount;
|
const amountChange = currentInvoice.amount - currentPayment.amount;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Card } from "../../components/generic/Styled";
|
import { Card } from '../../components/generic/Styled';
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
return <Card>Not Found</Card>;
|
return <Card>Not Found</Card>;
|
||||||
|
|
48
client/src/views/trader/MethodBoxes.tsx
Normal file
48
client/src/views/trader/MethodBoxes.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { themeColors, subCardColor } from 'styles/Themes';
|
||||||
|
|
||||||
|
interface MethodProps {
|
||||||
|
id: string;
|
||||||
|
payment_method_type: string;
|
||||||
|
payment_method_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MethodBoxesProps {
|
||||||
|
methods: MethodProps[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledMethodBoxes = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
right: -16px;
|
||||||
|
top: -26px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 0 0 -24px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMethod = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
border: 1px solid ${themeColors.blue2};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: ${subCardColor};
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MethodBoxes = ({ methods }: MethodBoxesProps) => {
|
||||||
|
if (!methods) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledMethodBoxes>
|
||||||
|
{methods.map((method) => (
|
||||||
|
<StyledMethod>{method.payment_method_name}</StyledMethod>
|
||||||
|
))}
|
||||||
|
</StyledMethodBoxes>
|
||||||
|
);
|
||||||
|
};
|
163
client/src/views/trader/Modal/FilterModal.tsx
Normal file
163
client/src/views/trader/Modal/FilterModal.tsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { SubTitle } from 'components/generic/Styled';
|
||||||
|
import { SortOptions, NewOptions } from '../OfferConfigs';
|
||||||
|
import { FilterType } from '../OfferFilters';
|
||||||
|
import { useQuery } from '@apollo/react-hooks';
|
||||||
|
import {
|
||||||
|
GET_HODL_COUNTRIES,
|
||||||
|
GET_HODL_CURRENCIES,
|
||||||
|
} from 'graphql/hodlhodl/query';
|
||||||
|
import { themeColors } from 'styles/Themes';
|
||||||
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
|
import { FilteredList } from './FilteredList';
|
||||||
|
import { OptionsLoading } from '../OfferCard.styled';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
interface FilterProps {
|
||||||
|
type: string;
|
||||||
|
dispatch: any;
|
||||||
|
final?: {};
|
||||||
|
newOptions?: FilterType[];
|
||||||
|
setModalType: (type: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CountryType {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
native_name: string;
|
||||||
|
currency_code: string;
|
||||||
|
currency_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrencyType {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterModal = ({
|
||||||
|
type,
|
||||||
|
dispatch,
|
||||||
|
final,
|
||||||
|
newOptions,
|
||||||
|
setModalType,
|
||||||
|
}: FilterProps) => {
|
||||||
|
const searchable: boolean = final?.['searchable'] || false;
|
||||||
|
const skipable: boolean = type !== 'Country' && type !== 'Currency';
|
||||||
|
|
||||||
|
const [selected, setSelected] = useState<{} | undefined>();
|
||||||
|
|
||||||
|
const [options, setOptions] = useState(newOptions ?? []);
|
||||||
|
const [title, setTitle] = useState(final?.['title'] || '');
|
||||||
|
|
||||||
|
const query = type === 'Country' ? GET_HODL_COUNTRIES : GET_HODL_CURRENCIES;
|
||||||
|
|
||||||
|
const { loading, data, error } = useQuery(query, {
|
||||||
|
skip: skipable,
|
||||||
|
onError: () => toast.error('Error Loading Options. Please try again.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (type) {
|
||||||
|
case 'sort':
|
||||||
|
setTitle('Sort Offers by:');
|
||||||
|
setOptions(SortOptions);
|
||||||
|
break;
|
||||||
|
case 'new':
|
||||||
|
setTitle('Add New Filter:');
|
||||||
|
setOptions(NewOptions);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && data && data.getCountries) {
|
||||||
|
const countryOptions = data.getCountries.map(
|
||||||
|
(country: CountryType) => {
|
||||||
|
const { code, name, native_name } = country;
|
||||||
|
return { name: code, title: `${name} (${native_name})` };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
setOptions(countryOptions);
|
||||||
|
}
|
||||||
|
if (!loading && data && data.getCurrencies) {
|
||||||
|
const filtered = data.getCurrencies.filter(
|
||||||
|
(currency: CurrencyType) => currency.type === 'fiat',
|
||||||
|
);
|
||||||
|
|
||||||
|
const currencyOptions = filtered.map((currency: CurrencyType) => {
|
||||||
|
const { code, name } = currency;
|
||||||
|
return { name: code, title: name };
|
||||||
|
});
|
||||||
|
|
||||||
|
setOptions(currencyOptions);
|
||||||
|
}
|
||||||
|
}, [data, loading]);
|
||||||
|
|
||||||
|
const handleClick = (name: string, option?: {}) => () => {
|
||||||
|
if (final) {
|
||||||
|
dispatch({
|
||||||
|
type: 'addFilter',
|
||||||
|
newItem: { [final['name']]: name },
|
||||||
|
});
|
||||||
|
setModalType('none');
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'sort':
|
||||||
|
if (name === 'none') {
|
||||||
|
dispatch({
|
||||||
|
type: 'removeSort',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: 'addSort',
|
||||||
|
newItem: { by: name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setModalType('none');
|
||||||
|
break;
|
||||||
|
case 'new':
|
||||||
|
setSelected(option);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterModal
|
||||||
|
type={selected['title']}
|
||||||
|
dispatch={dispatch}
|
||||||
|
final={selected}
|
||||||
|
newOptions={selected['options']}
|
||||||
|
setModalType={setModalType}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SubTitle>{title}</SubTitle>
|
||||||
|
<FilteredList
|
||||||
|
searchable={searchable}
|
||||||
|
options={options}
|
||||||
|
handleClick={handleClick}
|
||||||
|
/>
|
||||||
|
<OptionsLoading>
|
||||||
|
{loading && (
|
||||||
|
<ScaleLoader height={20} color={themeColors.blue3} />
|
||||||
|
)}
|
||||||
|
</OptionsLoading>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
76
client/src/views/trader/Modal/FilteredList.tsx
Normal file
76
client/src/views/trader/Modal/FilteredList.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||||
|
import { OfferModalBox } from '../OfferCard.styled';
|
||||||
|
import { Input } from 'components/input/Input';
|
||||||
|
import { Sub4Title } from 'components/generic/Styled';
|
||||||
|
|
||||||
|
interface FilteredProps {
|
||||||
|
searchable: boolean;
|
||||||
|
options: any;
|
||||||
|
handleClick: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilteredList = ({
|
||||||
|
searchable,
|
||||||
|
options,
|
||||||
|
handleClick,
|
||||||
|
}: FilteredProps) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
const [filteredOptions, setOptions] = useState<
|
||||||
|
{ name: string; title: string }[]
|
||||||
|
>(options);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = options.filter(
|
||||||
|
(option: { name: string; title: string }) => {
|
||||||
|
const inName = option.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase());
|
||||||
|
const inTitle = option.title
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
return inName || inTitle;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setOptions(filtered);
|
||||||
|
}, [searchTerm, options]);
|
||||||
|
|
||||||
|
const handleChange = (event: any) => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{searchable && (
|
||||||
|
<Input
|
||||||
|
placeholder={'Search'}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={handleChange}
|
||||||
|
withMargin={'0 0 8px 0'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<OfferModalBox>
|
||||||
|
{filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map(
|
||||||
|
(
|
||||||
|
option: { name: string; title: string },
|
||||||
|
index: number,
|
||||||
|
) => (
|
||||||
|
<ColorButton
|
||||||
|
key={`${index}-${option.name}`}
|
||||||
|
fullWidth={true}
|
||||||
|
withMargin={'0 0 2px 0'}
|
||||||
|
onClick={handleClick(option.name, option)}
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</ColorButton>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Sub4Title>No results</Sub4Title>
|
||||||
|
)}
|
||||||
|
</OfferModalBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
54
client/src/views/trader/OfferCard.styled.tsx
Normal file
54
client/src/views/trader/OfferCard.styled.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { unSelectedNavButton, mediaWidths } from 'styles/Themes';
|
||||||
|
import { ChevronRight } from 'components/generic/Icons';
|
||||||
|
|
||||||
|
export const TradesAmount = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${unSelectedNavButton};
|
||||||
|
margin: 0 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyleArrow = styled(ChevronRight)`
|
||||||
|
margin-bottom: -3px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OfferModalBox = styled.div`
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 640px;
|
||||||
|
min-height: 240px;
|
||||||
|
@media (${mediaWidths.mobile}) {
|
||||||
|
max-height: 240px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledTitle = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
@media (${mediaWidths.mobile}) {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledLogin = styled.div`
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledDescription = styled(StyledLogin)`
|
||||||
|
max-width: unset;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
max-height: 160px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OptionsLoading = styled.div`
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
195
client/src/views/trader/OfferCard.tsx
Normal file
195
client/src/views/trader/OfferCard.tsx
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SubCard,
|
||||||
|
Sub4Title,
|
||||||
|
SubTitle,
|
||||||
|
SingleLine,
|
||||||
|
ResponsiveLine,
|
||||||
|
Separation,
|
||||||
|
} from 'components/generic/Styled';
|
||||||
|
import { Rating } from 'components/rating/Rating';
|
||||||
|
import {
|
||||||
|
TradesAmount,
|
||||||
|
StyleArrow,
|
||||||
|
StyledTitle,
|
||||||
|
StyledLogin,
|
||||||
|
StyledDescription,
|
||||||
|
} from './OfferCard.styled';
|
||||||
|
import { MainInfo } from 'views/channels/Channels.style';
|
||||||
|
import { themeColors } from 'styles/Themes';
|
||||||
|
import { renderLine } from 'components/generic/Helpers';
|
||||||
|
import numeral from 'numeral';
|
||||||
|
import { MethodBoxes } from './MethodBoxes';
|
||||||
|
import { Link } from 'components/link/Link';
|
||||||
|
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||||
|
|
||||||
|
const format = (value: number | string, format: string = '0,0.00') =>
|
||||||
|
numeral(value).format(format);
|
||||||
|
|
||||||
|
interface OfferCardProps {
|
||||||
|
offer: any;
|
||||||
|
index: number;
|
||||||
|
setIndexOpen: (index: number) => void;
|
||||||
|
indexOpen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OfferCard = ({
|
||||||
|
offer,
|
||||||
|
index,
|
||||||
|
setIndexOpen,
|
||||||
|
indexOpen,
|
||||||
|
}: OfferCardProps) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
asset_code,
|
||||||
|
country,
|
||||||
|
country_code,
|
||||||
|
working_now,
|
||||||
|
side,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
currency_code,
|
||||||
|
price,
|
||||||
|
min_amount,
|
||||||
|
max_amount,
|
||||||
|
first_trade_limit,
|
||||||
|
fee,
|
||||||
|
balance,
|
||||||
|
payment_window_minutes,
|
||||||
|
confirmations,
|
||||||
|
payment_method_instructions,
|
||||||
|
trader,
|
||||||
|
} = offer;
|
||||||
|
|
||||||
|
const { author_fee_rate } = fee;
|
||||||
|
|
||||||
|
const {
|
||||||
|
login,
|
||||||
|
online_status,
|
||||||
|
rating,
|
||||||
|
trades_count = 0,
|
||||||
|
url,
|
||||||
|
verified,
|
||||||
|
verified_by,
|
||||||
|
strong_hodler,
|
||||||
|
country: traderCountry,
|
||||||
|
country_code: traderCode,
|
||||||
|
average_payment_time_minutes,
|
||||||
|
average_release_time_minutes,
|
||||||
|
days_since_last_trade,
|
||||||
|
} = trader;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (indexOpen === index) {
|
||||||
|
setIndexOpen(0);
|
||||||
|
} else {
|
||||||
|
setIndexOpen(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPayments = (): string => {
|
||||||
|
if (payment_method_instructions) {
|
||||||
|
const methods = payment_method_instructions.map(
|
||||||
|
(method: {
|
||||||
|
payment_method_name: string;
|
||||||
|
payment_method_type: string;
|
||||||
|
}) =>
|
||||||
|
`${method.payment_method_name} (${method.payment_method_type})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return methods.join(', ');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDetails = () => (
|
||||||
|
<>
|
||||||
|
<Separation />
|
||||||
|
<StyledDescription>{description}</StyledDescription>
|
||||||
|
<Separation />
|
||||||
|
{renderLine('Price', format(price))}
|
||||||
|
{renderLine('Min Amount:', format(min_amount))}
|
||||||
|
{renderLine('Max Amount:', format(max_amount))}
|
||||||
|
{renderLine(`First Trade Limit:`, format(first_trade_limit))}
|
||||||
|
{renderLine(`Payment Options:`, renderPayments())}
|
||||||
|
{renderLine('Country:', `${country} (${country_code})`)}
|
||||||
|
{renderLine('Available Now:', working_now ? 'Yes' : 'No')}
|
||||||
|
{renderLine(`Balance:`, format(balance))}
|
||||||
|
{renderLine(`Payment Window (min):`, payment_window_minutes)}
|
||||||
|
{renderLine(`Confirmations:`, confirmations)}
|
||||||
|
{renderLine(`Fee Rate:`, `${format(author_fee_rate * 100)}%`)}
|
||||||
|
<Separation />
|
||||||
|
<Sub4Title>Trader</Sub4Title>
|
||||||
|
{renderLine('User:', login)}
|
||||||
|
{renderLine('Online:', online_status)}
|
||||||
|
{renderLine('Rating:', rating)}
|
||||||
|
{renderLine('Amount of Trades:', trades_count)}
|
||||||
|
{renderLine('Verified:', verified)}
|
||||||
|
{renderLine('Verified By:', verified_by)}
|
||||||
|
{renderLine('Strong Hodler:', strong_hodler)}
|
||||||
|
{renderLine('Country:', `${traderCountry} (${traderCode})`)}
|
||||||
|
{renderLine(
|
||||||
|
'Average Payment Time (min):',
|
||||||
|
average_payment_time_minutes,
|
||||||
|
)}
|
||||||
|
{renderLine(
|
||||||
|
'Average Release Time (min):',
|
||||||
|
average_release_time_minutes,
|
||||||
|
)}
|
||||||
|
{renderLine('Days since last trade:', days_since_last_trade)}
|
||||||
|
<SingleLine>
|
||||||
|
<Link
|
||||||
|
href={`https://hodlhodl.com/offers/${id}`}
|
||||||
|
underline={'transparent'}
|
||||||
|
fullWidth={true}
|
||||||
|
>
|
||||||
|
<ColorButton
|
||||||
|
withBorder={true}
|
||||||
|
withMargin={'16px 8px 0 0'}
|
||||||
|
fullWidth={true}
|
||||||
|
>
|
||||||
|
View Offer
|
||||||
|
</ColorButton>
|
||||||
|
</Link>
|
||||||
|
<Link href={url} underline={'transparent'} fullWidth={true}>
|
||||||
|
<ColorButton
|
||||||
|
withBorder={true}
|
||||||
|
withMargin={'16px 0 0 8px'}
|
||||||
|
fullWidth={true}
|
||||||
|
>
|
||||||
|
View trader
|
||||||
|
</ColorButton>
|
||||||
|
</Link>
|
||||||
|
</SingleLine>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubCard withMargin={'16px 0 24px'} key={`${index}-${id}`}>
|
||||||
|
<MainInfo onClick={() => handleClick()}>
|
||||||
|
<MethodBoxes methods={payment_method_instructions} />
|
||||||
|
<ResponsiveLine>
|
||||||
|
<SubTitle>
|
||||||
|
{side !== 'buy' ? asset_code : currency_code}
|
||||||
|
<StyleArrow color={themeColors.blue3} />
|
||||||
|
{side !== 'buy' ? currency_code : asset_code}
|
||||||
|
</SubTitle>
|
||||||
|
<SingleLine>
|
||||||
|
<StyledLogin>{login}</StyledLogin>
|
||||||
|
{trades_count > 0 && (
|
||||||
|
<TradesAmount>{`(${trades_count}) `}</TradesAmount>
|
||||||
|
)}
|
||||||
|
<Rating rating={rating} />
|
||||||
|
</SingleLine>
|
||||||
|
</ResponsiveLine>
|
||||||
|
<StyledTitle>{title}</StyledTitle>
|
||||||
|
{renderLine('Price:', format(price))}
|
||||||
|
{renderLine(
|
||||||
|
`Min/Max amount:`,
|
||||||
|
`${format(min_amount, '0a')}/${format(max_amount, '0a')}`,
|
||||||
|
)}
|
||||||
|
</MainInfo>
|
||||||
|
{index === indexOpen && renderDetails()}
|
||||||
|
</SubCard>
|
||||||
|
);
|
||||||
|
};
|
141
client/src/views/trader/OfferConfigs.tsx
Normal file
141
client/src/views/trader/OfferConfigs.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
export const SortOptions = [
|
||||||
|
{
|
||||||
|
name: 'none',
|
||||||
|
title: 'None',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
title: 'Price',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'payment_window_minutes',
|
||||||
|
title: 'Payment Window',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user_average_payment_time_minutes',
|
||||||
|
title: 'Average Payment Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user_average_release_time_minutes',
|
||||||
|
title: 'Average Release Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rating',
|
||||||
|
title: 'Rating',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NewOptions = [
|
||||||
|
{
|
||||||
|
name: 'asset_code',
|
||||||
|
title: 'Asset Code',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'BTC',
|
||||||
|
title: 'Bitcoin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BTCLN',
|
||||||
|
title: 'Lightning Bitcoin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'side',
|
||||||
|
title: 'Side',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'buy',
|
||||||
|
title: 'I want to sell Bitcoin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sell',
|
||||||
|
title: 'I want to buy Bitcoin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'include_global',
|
||||||
|
title: 'Include Global Offers',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'true',
|
||||||
|
title: 'Yes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'false',
|
||||||
|
title: 'No',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'only_working_now',
|
||||||
|
title: 'Only Working Now Offers',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'true',
|
||||||
|
title: 'Yes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'false',
|
||||||
|
title: 'No',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'country',
|
||||||
|
title: 'Country',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currency_code',
|
||||||
|
title: 'Currency',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'payment_method_type',
|
||||||
|
title: 'Payment Type',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Bank wire',
|
||||||
|
title: 'Bank wire',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cash',
|
||||||
|
title: 'Cash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cryptocurrency',
|
||||||
|
title: 'Cryptocurrency',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Online payment system',
|
||||||
|
title: 'Online payment system',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'payment_method_id',
|
||||||
|
// title: 'Payment Id',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'payment_method_name',
|
||||||
|
// title: 'Payment Name',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'volume',
|
||||||
|
// title: 'Volume',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'payment_window_minutes_max',
|
||||||
|
// title: 'Max Payment Window',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'user_average_payment_time_minutes_max',
|
||||||
|
// title: 'Max User Payment Time',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'user_average_release_time_minutes_max',
|
||||||
|
// title: 'Max User Release Time',
|
||||||
|
// },
|
||||||
|
];
|
253
client/src/views/trader/OfferFilters.tsx
Normal file
253
client/src/views/trader/OfferFilters.tsx
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import React, { useState, useReducer } from 'react';
|
||||||
|
import {
|
||||||
|
SingleLine,
|
||||||
|
Separation,
|
||||||
|
ResponsiveLine,
|
||||||
|
NoWrapTitle,
|
||||||
|
} from 'components/generic/Styled';
|
||||||
|
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||||
|
import {
|
||||||
|
MultiButton,
|
||||||
|
SingleButton,
|
||||||
|
} from 'components/buttons/multiButton/MultiButton';
|
||||||
|
import Modal from 'components/modal/ReactModal';
|
||||||
|
import { FilterModal } from './Modal/FilterModal';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { SortOptions } from './OfferConfigs';
|
||||||
|
import { QueryProps } from './TraderView';
|
||||||
|
import { XSvg } from 'components/generic/Icons';
|
||||||
|
import { renderLine } from 'components/generic/Helpers';
|
||||||
|
import { chartColors } from 'styles/Themes';
|
||||||
|
|
||||||
|
type ActionType = {
|
||||||
|
type:
|
||||||
|
| 'addFilter'
|
||||||
|
| 'addSort'
|
||||||
|
| 'removeSort'
|
||||||
|
| 'removeFilter'
|
||||||
|
| 'changeLimit';
|
||||||
|
state?: QueryProps;
|
||||||
|
newItem?: {};
|
||||||
|
removeKey?: string;
|
||||||
|
changeLimit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state: QueryProps, action: ActionType): QueryProps => {
|
||||||
|
const { sort, filters } = state;
|
||||||
|
switch (action.type) {
|
||||||
|
case 'addSort':
|
||||||
|
let direction = {};
|
||||||
|
if (sort && !sort.direction) {
|
||||||
|
direction = { direction: 'desc' };
|
||||||
|
}
|
||||||
|
const newSort = { ...sort, ...direction, ...action.newItem };
|
||||||
|
return { ...state, sort: newSort };
|
||||||
|
case 'removeSort':
|
||||||
|
return { ...state, sort: { by: '', direction: '' } };
|
||||||
|
case 'addFilter':
|
||||||
|
const newFilters = { ...filters, ...action.newItem };
|
||||||
|
return { ...state, filters: newFilters };
|
||||||
|
case 'removeFilter':
|
||||||
|
if (action.removeKey) {
|
||||||
|
const remaining = { ...filters };
|
||||||
|
delete remaining[action.removeKey];
|
||||||
|
return { ...state, filters: remaining };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
case 'changeLimit':
|
||||||
|
if (action.changeLimit) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pagination: { limit: action.changeLimit, offset: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
default:
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FilterProps {
|
||||||
|
offerFilters: QueryProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterType {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
optionOne?: string;
|
||||||
|
optionTwo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OfferFilters = ({ offerFilters }: FilterProps) => {
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
const [filterState, dispatch] = useReducer(reducer, offerFilters);
|
||||||
|
|
||||||
|
const [modalType, setModalType] = useState<string>('none');
|
||||||
|
const [willApply, setWillApply] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const renderButton = (
|
||||||
|
onClick: () => void,
|
||||||
|
text: string,
|
||||||
|
selected: boolean,
|
||||||
|
) => (
|
||||||
|
<SingleButton selected={selected} onClick={onClick}>
|
||||||
|
{text}
|
||||||
|
</SingleButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSave = () =>
|
||||||
|
push({ search: `?filter=${btoa(JSON.stringify(filterState))}` });
|
||||||
|
|
||||||
|
const handleRemoveAll = () => push({ search: '' });
|
||||||
|
|
||||||
|
const handleRemove = (removeKey: string) => {
|
||||||
|
dispatch({ type: 'removeFilter', removeKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAppliedFilters = () => {
|
||||||
|
const currentFilters = filterState.filters;
|
||||||
|
let activeFilters = [];
|
||||||
|
for (const key in currentFilters) {
|
||||||
|
if (currentFilters.hasOwnProperty(key)) {
|
||||||
|
const element = currentFilters[key];
|
||||||
|
activeFilters.push(
|
||||||
|
renderLine(key, element, `${key}-${element}`, () =>
|
||||||
|
handleRemove(key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <>{activeFilters.map((filter) => filter)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLimitOptions = () => {
|
||||||
|
const currentLimit = filterState.pagination.limit;
|
||||||
|
|
||||||
|
const renderButton = (value: number, margin?: string) => (
|
||||||
|
<ColorButton
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({ type: 'changeLimit', changeLimit: value })
|
||||||
|
}
|
||||||
|
selected={currentLimit === value}
|
||||||
|
withMargin={margin}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</ColorButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SingleLine>
|
||||||
|
{renderButton(25)}
|
||||||
|
{renderButton(50, '0 4px 0')}
|
||||||
|
{renderButton(100)}
|
||||||
|
</SingleLine>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFilters = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separation />
|
||||||
|
<ResponsiveLine>
|
||||||
|
<NoWrapTitle>Results:</NoWrapTitle>
|
||||||
|
{renderLimitOptions()}
|
||||||
|
</ResponsiveLine>
|
||||||
|
<Separation />
|
||||||
|
<ResponsiveLine>
|
||||||
|
<NoWrapTitle>Filters</NoWrapTitle>
|
||||||
|
<ColorButton
|
||||||
|
arrow={true}
|
||||||
|
onClick={() => setModalType('new')}
|
||||||
|
>
|
||||||
|
New Filter
|
||||||
|
</ColorButton>
|
||||||
|
</ResponsiveLine>
|
||||||
|
{Object.keys(filterState.filters).length > 0 &&
|
||||||
|
renderAppliedFilters()}
|
||||||
|
<Separation />
|
||||||
|
<ResponsiveLine>
|
||||||
|
<NoWrapTitle>Sort By:</NoWrapTitle>
|
||||||
|
<ColorButton
|
||||||
|
arrow={true}
|
||||||
|
onClick={() => setModalType('sort')}
|
||||||
|
>
|
||||||
|
{SortOptions.find(
|
||||||
|
(option) => option.name === filterState.sort.by,
|
||||||
|
)?.title ?? 'None'}
|
||||||
|
</ColorButton>
|
||||||
|
</ResponsiveLine>
|
||||||
|
{!!filterState.sort.by && (
|
||||||
|
<ResponsiveLine>
|
||||||
|
<NoWrapTitle>Sort Direction:</NoWrapTitle>
|
||||||
|
<MultiButton>
|
||||||
|
{renderButton(
|
||||||
|
() => {
|
||||||
|
dispatch({
|
||||||
|
type: 'addSort',
|
||||||
|
newItem: { direction: 'asc' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'Asc',
|
||||||
|
filterState.sort.direction === 'asc',
|
||||||
|
)}
|
||||||
|
{renderButton(
|
||||||
|
() => {
|
||||||
|
dispatch({
|
||||||
|
type: 'addSort',
|
||||||
|
newItem: { direction: 'desc' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'Desc',
|
||||||
|
filterState.sort.direction !== 'asc',
|
||||||
|
)}
|
||||||
|
</MultiButton>
|
||||||
|
</ResponsiveLine>
|
||||||
|
)}
|
||||||
|
<Separation />
|
||||||
|
<SingleLine>
|
||||||
|
<ColorButton
|
||||||
|
fullWidth={true}
|
||||||
|
withMargin={'16px 4px 0 0'}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</ColorButton>
|
||||||
|
<ColorButton
|
||||||
|
color={chartColors.orange2}
|
||||||
|
fullWidth={true}
|
||||||
|
withMargin={'16px 0 0 4px'}
|
||||||
|
onClick={handleRemoveAll}
|
||||||
|
>
|
||||||
|
Remove All
|
||||||
|
</ColorButton>
|
||||||
|
</SingleLine>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SingleLine>
|
||||||
|
Filters
|
||||||
|
<ColorButton
|
||||||
|
arrow={!willApply}
|
||||||
|
onClick={() => setWillApply((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{willApply ? <XSvg /> : 'Apply'}
|
||||||
|
</ColorButton>
|
||||||
|
</SingleLine>
|
||||||
|
{willApply && renderFilters()}
|
||||||
|
<Modal
|
||||||
|
isOpen={modalType !== 'none'}
|
||||||
|
closeCallback={() => setModalType('none')}
|
||||||
|
>
|
||||||
|
<FilterModal
|
||||||
|
type={modalType}
|
||||||
|
dispatch={dispatch}
|
||||||
|
setModalType={setModalType}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
148
client/src/views/trader/TraderView.tsx
Normal file
148
client/src/views/trader/TraderView.tsx
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
CardWithTitle,
|
||||||
|
SubTitle,
|
||||||
|
Card,
|
||||||
|
DarkSubTitle,
|
||||||
|
ResponsiveLine,
|
||||||
|
} from 'components/generic/Styled';
|
||||||
|
import { useQuery } from '@apollo/react-hooks';
|
||||||
|
import { GET_HODL_OFFERS } from 'graphql/hodlhodl/query';
|
||||||
|
import { LoadingCard } from 'components/loading/LoadingCard';
|
||||||
|
import { OfferCard } from './OfferCard';
|
||||||
|
import { OfferFilters } from './OfferFilters';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import qs from 'qs';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Link } from 'components/link/Link';
|
||||||
|
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||||
|
|
||||||
|
export interface QueryProps {
|
||||||
|
pagination: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
filters: {};
|
||||||
|
sort: {
|
||||||
|
by: string;
|
||||||
|
direction: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultQuery: QueryProps = {
|
||||||
|
pagination: {
|
||||||
|
limit: 25,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
filters: {},
|
||||||
|
sort: {
|
||||||
|
by: '',
|
||||||
|
direction: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TraderView = () => {
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
const query = qs.parse(search, { ignoreQueryPrefix: true });
|
||||||
|
|
||||||
|
let decoded: QueryProps = defaultQuery;
|
||||||
|
if (!!query.filter) {
|
||||||
|
try {
|
||||||
|
decoded = JSON.parse(atob(query.filter));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Incorrect url.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryObject = {
|
||||||
|
...defaultQuery,
|
||||||
|
...decoded,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [indexOpen, setIndexOpen] = useState(0);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
|
||||||
|
const { data, loading, fetchMore, error } = useQuery(GET_HODL_OFFERS, {
|
||||||
|
variables: { filter: JSON.stringify(queryObject) },
|
||||||
|
onError: () => toast.error('Error getting offers. Please try again.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading || !data || !data.getOffers) {
|
||||||
|
return <LoadingCard title={'P2P Trading'} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountOfOffers = data.getOffers.length;
|
||||||
|
const {
|
||||||
|
pagination: { limit },
|
||||||
|
} = queryObject;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWithTitle>
|
||||||
|
<ResponsiveLine>
|
||||||
|
<SubTitle>P2P Trading</SubTitle>
|
||||||
|
<DarkSubTitle>
|
||||||
|
Powered by{' '}
|
||||||
|
<Link href={'https://hodlhodl.com/'}>HodlHodl</Link>
|
||||||
|
</DarkSubTitle>
|
||||||
|
</ResponsiveLine>
|
||||||
|
<Card bottom={'16px'}>
|
||||||
|
<OfferFilters offerFilters={queryObject} />
|
||||||
|
</Card>
|
||||||
|
<Card bottom={'8px'}>
|
||||||
|
{amountOfOffers <= 0 && (
|
||||||
|
<DarkSubTitle>No Offers Found</DarkSubTitle>
|
||||||
|
)}
|
||||||
|
{data.getOffers.map((offer: any, index: number) => (
|
||||||
|
<OfferCard
|
||||||
|
offer={offer}
|
||||||
|
index={index + 1}
|
||||||
|
setIndexOpen={setIndexOpen}
|
||||||
|
indexOpen={indexOpen}
|
||||||
|
key={`${index}-${offer.id}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
{amountOfOffers > 0 && amountOfOffers === limit * page && (
|
||||||
|
<ColorButton
|
||||||
|
loading={fetching}
|
||||||
|
disabled={fetching}
|
||||||
|
onClick={() => {
|
||||||
|
setFetching(true);
|
||||||
|
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
filter: JSON.stringify({
|
||||||
|
...queryObject,
|
||||||
|
pagination: { limit, offset: limit * page },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
updateQuery: (
|
||||||
|
prev,
|
||||||
|
{ fetchMoreResult: result },
|
||||||
|
) => {
|
||||||
|
if (!result) return prev;
|
||||||
|
|
||||||
|
setFetching(false);
|
||||||
|
setPage((prev) => (prev += 1));
|
||||||
|
return {
|
||||||
|
getOffers: [
|
||||||
|
...prev.getOffers,
|
||||||
|
...result.getOffers,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show More
|
||||||
|
</ColorButton>
|
||||||
|
)}
|
||||||
|
</CardWithTitle>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,8 +8,6 @@ import { toast } from 'react-toastify';
|
||||||
import { getErrorContent } from '../../utils/error';
|
import { getErrorContent } from '../../utils/error';
|
||||||
import { PaymentsCard } from './PaymentsCards';
|
import { PaymentsCard } from './PaymentsCards';
|
||||||
import { LoadingCard } from '../../components/loading/LoadingCard';
|
import { LoadingCard } from '../../components/loading/LoadingCard';
|
||||||
import { useSettings } from '../../context/SettingsContext';
|
|
||||||
import { textColorMap } from '../../styles/Themes';
|
|
||||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||||
import { FlowBox } from 'views/home/reports/flow';
|
import { FlowBox } from 'views/home/reports/flow';
|
||||||
|
|
||||||
|
@ -18,7 +16,6 @@ export const TransactionList = () => {
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
|
|
||||||
const { theme } = useSettings();
|
|
||||||
const { host, viewOnly, cert, sessionAdmin } = useAccount();
|
const { host, viewOnly, cert, sessionAdmin } = useAccount();
|
||||||
const auth = {
|
const auth = {
|
||||||
host,
|
host,
|
||||||
|
@ -48,7 +45,7 @@ export const TransactionList = () => {
|
||||||
<FlowBox />
|
<FlowBox />
|
||||||
<CardWithTitle>
|
<CardWithTitle>
|
||||||
<SubTitle>Transactions</SubTitle>
|
<SubTitle>Transactions</SubTitle>
|
||||||
<Card bottom={'5px'}>
|
<Card bottom={'8px'}>
|
||||||
{resumeList.map((entry: any, index: number) => {
|
{resumeList.map((entry: any, index: number) => {
|
||||||
if (entry.type === 'invoice') {
|
if (entry.type === 'invoice') {
|
||||||
return (
|
return (
|
||||||
|
@ -74,9 +71,7 @@ export const TransactionList = () => {
|
||||||
})}
|
})}
|
||||||
</Card>
|
</Card>
|
||||||
<ColorButton
|
<ColorButton
|
||||||
selected={true}
|
|
||||||
loading={fetching}
|
loading={fetching}
|
||||||
color={textColorMap[theme]}
|
|
||||||
disabled={fetching}
|
disabled={fetching}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFetching(true);
|
setFetching(true);
|
||||||
|
|
6
lerna.json
Normal file
6
lerna.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"packages": ["client", "server"],
|
||||||
|
"npmClient": "yarn",
|
||||||
|
"useWorkspaces": true,
|
||||||
|
"version": "0.2.0"
|
||||||
|
}
|
31
package.json
Normal file
31
package.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"server:dev": "lerna run build:dev --stream --scope @thunderhub/server",
|
||||||
|
"server:prod": "lerna run build --stream --scope @thunderhub/server",
|
||||||
|
"server:run": "lerna run start --stream --scope @thunderhub/server"
|
||||||
|
},
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"client",
|
||||||
|
"server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"base64url": "^3.0.1",
|
||||||
|
"date-fns": "^2.0.0-beta.5",
|
||||||
|
"graphql": "^14.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"husky": "^4.2.3",
|
||||||
|
"lerna": "^3.20.2",
|
||||||
|
"prettier": "2.0.2",
|
||||||
|
"pretty-quick": "^2.0.1"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npx lerna run --concurrency 1 --stream precommit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "thunderhub-server",
|
"name": "@thunderhub/server",
|
||||||
"version": "0.1.17",
|
"version": "0.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build": "webpack --config webpack.production.js",
|
"build": "webpack --config webpack.production.js",
|
||||||
"build:dev": "webpack --config webpack.development.js",
|
"build:dev": "webpack --config webpack.development.js",
|
||||||
"start": "node dist/server",
|
"start": "node --http-parser=legacy dist/server",
|
||||||
"dev": "nodemon dist/server",
|
"dev": "nodemon --http-parser=legacy dist/server",
|
||||||
"deploy": "yarn build && eb deploy"
|
"deploy": "yarn build && eb deploy",
|
||||||
|
"precommit": "pretty-quick --staged"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -23,10 +24,7 @@
|
||||||
"@types/node-fetch": "^2.5.5",
|
"@types/node-fetch": "^2.5.5",
|
||||||
"@types/underscore": "^1.9.4",
|
"@types/underscore": "^1.9.4",
|
||||||
"apollo-server": "^2.11.0",
|
"apollo-server": "^2.11.0",
|
||||||
"base64url": "^3.0.1",
|
|
||||||
"date-fns": "^2.11.0",
|
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"graphql": "^14.6.0",
|
|
||||||
"graphql-depth-limit": "^1.1.0",
|
"graphql-depth-limit": "^1.1.0",
|
||||||
"graphql-iso-date": "^3.6.1",
|
"graphql-iso-date": "^3.6.1",
|
||||||
"graphql-rate-limit": "^2.0.1",
|
"graphql-rate-limit": "^2.0.1",
|
||||||
|
@ -37,19 +35,12 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/webpack-env": "^1.15.1",
|
"@types/webpack-env": "^1.15.1",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"husky": "^4.2.3",
|
|
||||||
"prettier": "^2.0.2",
|
|
||||||
"pretty-quick": "^2.0.0",
|
|
||||||
"ts-loader": "^6.2.2",
|
"ts-loader": "^6.2.2",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
"webpack": "^4.42.1",
|
"webpack": "^4.42.1",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-merge": "^4.2.2",
|
"webpack-merge": "^4.2.2",
|
||||||
"webpack-node-externals": "^1.7.2"
|
"webpack-node-externals": "^1.7.2",
|
||||||
},
|
"webpack-shell-plugin": "^0.5.0"
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "pretty-quick --staged"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import base64url from 'base64url';
|
|
||||||
import { authenticatedLndGrpc } from 'ln-service';
|
import { authenticatedLndGrpc } from 'ln-service';
|
||||||
|
import { envConfig } from '../utils/envConfig';
|
||||||
|
|
||||||
export const getIp = (req: any) => {
|
export const getIp = (req: any) => {
|
||||||
if (!req || !req.headers) {
|
if (!req || !req.headers) {
|
||||||
|
@ -9,7 +9,7 @@ export const getIp = (req: any) => {
|
||||||
const before = forwarded
|
const before = forwarded
|
||||||
? forwarded.split(/, /)[0]
|
? forwarded.split(/, /)[0]
|
||||||
: req.connection.remoteAddress;
|
: req.connection.remoteAddress;
|
||||||
const ip = process.env.NODE_ENV === 'development' ? '1.2.3.4' : before;
|
const ip = envConfig.env === 'development' ? '1.2.3.4' : before;
|
||||||
return ip;
|
return ip;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
18
server/src/helpers/hodlHelpers.ts
Normal file
18
server/src/helpers/hodlHelpers.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export const getHodlParams = (params: any): string => {
|
||||||
|
let paramString = '?';
|
||||||
|
|
||||||
|
for (const key in params) {
|
||||||
|
if (params.hasOwnProperty(key)) {
|
||||||
|
const element = params[key];
|
||||||
|
|
||||||
|
for (const subKey in element) {
|
||||||
|
if (element.hasOwnProperty(subKey)) {
|
||||||
|
const subElement = element[subKey];
|
||||||
|
paramString = `${paramString}&${key}[${subKey}]=${subElement}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramString;
|
||||||
|
};
|
|
@ -1,38 +1,43 @@
|
||||||
import { createLogger, format, transports } from "winston";
|
import { createLogger, format, transports } from 'winston';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
|
import { envConfig } from '../utils/envConfig';
|
||||||
|
|
||||||
const combinedFormat =
|
const combinedFormat =
|
||||||
process.env.NODE_ENV === "development"
|
envConfig.env === 'development'
|
||||||
? format.combine(
|
? format.combine(
|
||||||
format.label({
|
format.label({
|
||||||
label: path.basename(
|
label: path.basename(
|
||||||
process && process.mainModule ? process.mainModule.filename : ""
|
process && process.mainModule
|
||||||
)
|
? process.mainModule.filename
|
||||||
|
: '',
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
format.splat(),
|
format.splat(),
|
||||||
format.colorize(),
|
format.colorize(),
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
format.printf(
|
format.printf(
|
||||||
(info: any) =>
|
(info: any) =>
|
||||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
|
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
: format.combine(
|
: format.combine(
|
||||||
format.label({
|
format.label({
|
||||||
label: path.basename(
|
label: path.basename(
|
||||||
process && process.mainModule ? process.mainModule.filename : ""
|
process && process.mainModule
|
||||||
)
|
? process.mainModule.filename
|
||||||
|
: '',
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
format.splat(),
|
format.splat(),
|
||||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
format.printf(
|
format.printf(
|
||||||
(info: any) =>
|
(info: any) =>
|
||||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
|
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const logger = createLogger({
|
export const logger = createLogger({
|
||||||
level: process.env.LOG_LEVEL || "silly",
|
level: envConfig.logLevel,
|
||||||
format: combinedFormat,
|
format: combinedFormat,
|
||||||
transports: [new transports.Console()]
|
transports: [new transports.Console()],
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { thunderHubSchema } from './schemas';
|
||||||
import { logger } from './helpers/logger';
|
import { logger } from './helpers/logger';
|
||||||
import { getIp } from './helpers/helpers';
|
import { getIp } from './helpers/helpers';
|
||||||
import depthLimit from 'graphql-depth-limit';
|
import depthLimit from 'graphql-depth-limit';
|
||||||
|
import { envConfig } from './utils/envConfig';
|
||||||
|
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
schema: thunderHubSchema,
|
schema: thunderHubSchema,
|
||||||
|
@ -15,7 +16,7 @@ const server = new ApolloServer({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen({ port: process.env.PORT || 3001 }).then(({ url }: any) => {
|
server.listen({ port: envConfig.port }).then(({ url }: any) => {
|
||||||
logger.info(`Server ready at ${url}`);
|
logger.info(`Server ready at ${url}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { GraphQLSchema, GraphQLObjectType } from "graphql";
|
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
|
||||||
import { query } from "./query";
|
import { query } from './query';
|
||||||
import { mutation } from "./mutations";
|
import { mutation } from './mutations';
|
||||||
|
|
||||||
export const thunderHubSchema = new GraphQLSchema({
|
export const thunderHubSchema = new GraphQLSchema({
|
||||||
query: new GraphQLObjectType({
|
query: new GraphQLObjectType({
|
||||||
name: "Query",
|
name: 'Query',
|
||||||
fields: query
|
fields: query,
|
||||||
}),
|
}),
|
||||||
mutation: new GraphQLObjectType({
|
mutation: new GraphQLObjectType({
|
||||||
name: "Mutation",
|
name: 'Mutation',
|
||||||
fields: mutation
|
fields: mutation,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||||
import { GraphQLBoolean } from 'graphql';
|
import { GraphQLBoolean } from 'graphql';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { BitcoinFeeType } from '../../types/QueryType';
|
import { BitcoinFeeType } from '../../types/QueryType';
|
||||||
|
import { appUrls } from '../../../utils/appUrls';
|
||||||
const url = 'https://bitcoinfees.earn.com/api/v1/fees/recommended';
|
|
||||||
|
|
||||||
export const getBitcoinFees = {
|
export const getBitcoinFees = {
|
||||||
type: BitcoinFeeType,
|
type: BitcoinFeeType,
|
||||||
|
@ -15,7 +14,7 @@ export const getBitcoinFees = {
|
||||||
await requestLimiter(context.ip, 'bitcoinFee');
|
await requestLimiter(context.ip, 'bitcoinFee');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(appUrls.fees);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
if (json) {
|
if (json) {
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { logger } from '../../../helpers/logger';
|
||||||
import { requestLimiter } from '../../../helpers/rateLimiter';
|
import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||||
import { GraphQLString, GraphQLBoolean } from 'graphql';
|
import { GraphQLString, GraphQLBoolean } from 'graphql';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
import { appUrls } from '../../../utils/appUrls';
|
||||||
const url = 'https://blockchain.info/ticker';
|
|
||||||
|
|
||||||
export const getBitcoinPrice = {
|
export const getBitcoinPrice = {
|
||||||
type: GraphQLString,
|
type: GraphQLString,
|
||||||
|
@ -17,7 +16,7 @@ export const getBitcoinPrice = {
|
||||||
await requestLimiter(context.ip, 'bitcoinPrice');
|
await requestLimiter(context.ip, 'bitcoinPrice');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(appUrls.ticker);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
return JSON.stringify(json);
|
return JSON.stringify(json);
|
||||||
|
|
37
server/src/schemas/query/hodlhodl/getCountries.ts
Normal file
37
server/src/schemas/query/hodlhodl/getCountries.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { GraphQLList } from 'graphql';
|
||||||
|
import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||||
|
import { logger } from '../../../helpers/logger';
|
||||||
|
import { appUrls } from '../../../utils/appUrls';
|
||||||
|
import { HodlCountryType } from '../../types/HodlType';
|
||||||
|
import { envConfig } from '../../../utils/envConfig';
|
||||||
|
|
||||||
|
export const getCountries = {
|
||||||
|
type: new GraphQLList(HodlCountryType),
|
||||||
|
args: {},
|
||||||
|
resolve: async (root: any, params: any, context: any) => {
|
||||||
|
await requestLimiter(context.ip, 'getCountries');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${envConfig.hodlKey}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${appUrls.hodlhodl}/v1/countries`, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
const { countries } = json;
|
||||||
|
return countries;
|
||||||
|
} else {
|
||||||
|
throw new Error('Problem getting HodlHodl countries.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
params.logger &&
|
||||||
|
logger.error('Error getting HodlHodl countries: %o', error);
|
||||||
|
throw new Error('Problem getting HodlHodl countries.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
37
server/src/schemas/query/hodlhodl/getCurrencies.ts
Normal file
37
server/src/schemas/query/hodlhodl/getCurrencies.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { GraphQLList } from 'graphql';
|
||||||
|
import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||||
|
import { logger } from '../../../helpers/logger';
|
||||||
|
import { appUrls } from '../../../utils/appUrls';
|
||||||
|
import { HodlCurrencyType } from '../../types/HodlType';
|
||||||
|
import { envConfig } from '../../../utils/envConfig';
|
||||||
|
|
||||||
|
export const getCurrencies = {
|
||||||
|
type: new GraphQLList(HodlCurrencyType),
|
||||||
|
args: {},
|
||||||
|
resolve: async (root: any, params: any, context: any) => {
|
||||||
|
await requestLimiter(context.ip, 'getCurrencies');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${envConfig.hodlKey}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${appUrls.hodlhodl}/v1/currencies`, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
const { currencies } = json;
|
||||||
|
return currencies;
|
||||||
|
} else {
|
||||||
|
throw new Error('Problem getting HodlHodl currencies.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
params.logger &&
|
||||||
|
logger.error('Error getting HodlHodl currencies: %o', error);
|
||||||
|
throw new Error('Problem getting HodlHodl currencies.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
57
server/src/schemas/query/hodlhodl/getOffers.ts
Normal file
57
server/src/schemas/query/hodlhodl/getOffers.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { GraphQLList, GraphQLString } from 'graphql';
|
||||||
|
import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||||
|
import { logger } from '../../../helpers/logger';
|
||||||
|
import { appUrls } from '../../../utils/appUrls';
|
||||||
|
import { HodlOfferType } from '../../types/HodlType';
|
||||||
|
import { getHodlParams } from '../../../helpers/hodlHelpers';
|
||||||
|
|
||||||
|
const defaultQuery = {
|
||||||
|
filters: {},
|
||||||
|
sort: {
|
||||||
|
by: '',
|
||||||
|
direction: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOffers = {
|
||||||
|
type: new GraphQLList(HodlOfferType),
|
||||||
|
args: {
|
||||||
|
filter: { type: GraphQLString },
|
||||||
|
},
|
||||||
|
resolve: async (root: any, params: any, context: any) => {
|
||||||
|
await requestLimiter(context.ip, 'getOffers');
|
||||||
|
|
||||||
|
let queryParams = defaultQuery;
|
||||||
|
|
||||||
|
if (params.filter) {
|
||||||
|
try {
|
||||||
|
queryParams = JSON.parse(params.filter);
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullParams = {
|
||||||
|
...queryParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paramString = getHodlParams(fullParams);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${appUrls.hodlhodl}/v1/offers${paramString}`,
|
||||||
|
);
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
const { offers } = json;
|
||||||
|
return offers;
|
||||||
|
} else {
|
||||||
|
throw new Error('Problem getting HodlHodl offers.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
params.logger &&
|
||||||
|
logger.error('Error getting HodlHodl offers: %o', error);
|
||||||
|
throw new Error('Problem getting HodlHodl offers.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
9
server/src/schemas/query/hodlhodl/index.ts
Normal file
9
server/src/schemas/query/hodlhodl/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { getOffers } from './getOffers';
|
||||||
|
import { getCountries } from './getCountries';
|
||||||
|
import { getCurrencies } from './getCurrencies';
|
||||||
|
|
||||||
|
export const hodlQueries = {
|
||||||
|
getOffers,
|
||||||
|
getCountries,
|
||||||
|
getCurrencies,
|
||||||
|
};
|
|
@ -9,6 +9,7 @@ import { routeQueries } from './route';
|
||||||
import { peerQueries } from './peer';
|
import { peerQueries } from './peer';
|
||||||
import { messageQueries } from './message';
|
import { messageQueries } from './message';
|
||||||
import { chainQueries } from './chain';
|
import { chainQueries } from './chain';
|
||||||
|
import { hodlQueries } from './hodlhodl';
|
||||||
|
|
||||||
export const query = {
|
export const query = {
|
||||||
...channelQueries,
|
...channelQueries,
|
||||||
|
@ -22,4 +23,5 @@ export const query = {
|
||||||
...peerQueries,
|
...peerQueries,
|
||||||
...messageQueries,
|
...messageQueries,
|
||||||
...chainQueries,
|
...chainQueries,
|
||||||
|
...hodlQueries,
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const getForwardChannelsReport = {
|
||||||
|
|
||||||
const getRouteAlias = (array: any[], publicKey: string) =>
|
const getRouteAlias = (array: any[], publicKey: string) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
array.map(async channel => {
|
array.map(async (channel) => {
|
||||||
const nodeAliasIn = await getNodeAlias(
|
const nodeAliasIn = await getNodeAlias(
|
||||||
channel.in,
|
channel.in,
|
||||||
publicKey,
|
publicKey,
|
||||||
|
@ -94,7 +94,7 @@ export const getForwardChannelsReport = {
|
||||||
|
|
||||||
const getAlias = (array: any[], publicKey: string) =>
|
const getAlias = (array: any[], publicKey: string) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
array.map(async channel => {
|
array.map(async (channel) => {
|
||||||
const nodeAlias = await getNodeAlias(
|
const nodeAlias = await getNodeAlias(
|
||||||
channel.name,
|
channel.name,
|
||||||
publicKey,
|
publicKey,
|
||||||
|
@ -120,7 +120,7 @@ export const getForwardChannelsReport = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.type === 'route') {
|
if (params.type === 'route') {
|
||||||
const mapped = forwardsList.forwards.map(forward => {
|
const mapped = forwardsList.forwards.map((forward) => {
|
||||||
return {
|
return {
|
||||||
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
|
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
|
||||||
...forward,
|
...forward,
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const getForwardReport = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (params.time === 'month' || params.time === 'week') {
|
if (params.time === 'month' || params.time === 'week') {
|
||||||
const orderedDay = groupBy(forwardsList.forwards, item => {
|
const orderedDay = groupBy(forwardsList.forwards, (item) => {
|
||||||
return (
|
return (
|
||||||
days -
|
days -
|
||||||
differenceInCalendarDays(
|
differenceInCalendarDays(
|
||||||
|
@ -60,7 +60,7 @@ export const getForwardReport = {
|
||||||
|
|
||||||
return JSON.stringify(reducedOrderedDay);
|
return JSON.stringify(reducedOrderedDay);
|
||||||
} else {
|
} else {
|
||||||
const orderedHour = groupBy(forwardsList.forwards, item => {
|
const orderedHour = groupBy(forwardsList.forwards, (item) => {
|
||||||
return (
|
return (
|
||||||
24 -
|
24 -
|
||||||
differenceInHours(endDate, new Date(item.created_at))
|
differenceInHours(endDate, new Date(item.created_at))
|
||||||
|
|
|
@ -56,11 +56,11 @@ export const countArray = (list: ForwardProps[], type: boolean) => {
|
||||||
const element = grouped[key];
|
const element = grouped[key];
|
||||||
|
|
||||||
const fee = element
|
const fee = element
|
||||||
.map(forward => forward.fee)
|
.map((forward) => forward.fee)
|
||||||
.reduce((p, c) => p + c);
|
.reduce((p, c) => p + c);
|
||||||
|
|
||||||
const tokens = element
|
const tokens = element
|
||||||
.map(forward => forward.tokens)
|
.map((forward) => forward.tokens)
|
||||||
.reduce((p, c) => p + c);
|
.reduce((p, c) => p + c);
|
||||||
|
|
||||||
channelInfo.push({
|
channelInfo.push({
|
||||||
|
@ -84,11 +84,11 @@ export const countRoutes = (list: ForwardProps[]) => {
|
||||||
const element = grouped[key];
|
const element = grouped[key];
|
||||||
|
|
||||||
const fee = element
|
const fee = element
|
||||||
.map(forward => forward.fee)
|
.map((forward) => forward.fee)
|
||||||
.reduce((p, c) => p + c);
|
.reduce((p, c) => p + c);
|
||||||
|
|
||||||
const tokens = element
|
const tokens = element
|
||||||
.map(forward => forward.tokens)
|
.map((forward) => forward.tokens)
|
||||||
.reduce((p, c) => p + c);
|
.reduce((p, c) => p + c);
|
||||||
|
|
||||||
channelInfo.push({
|
channelInfo.push({
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getForwardReport } from "./ForwardReport";
|
import { getForwardReport } from './ForwardReport';
|
||||||
import { getForwardChannelsReport } from "./ForwardChannels";
|
import { getForwardChannelsReport } from './ForwardChannels';
|
||||||
|
|
||||||
export const reportQueries = {
|
export const reportQueries = {
|
||||||
getForwardReport,
|
getForwardReport,
|
||||||
getForwardChannelsReport
|
getForwardChannelsReport,
|
||||||
};
|
};
|
||||||
|
|
105
server/src/schemas/types/HodlType.ts
Normal file
105
server/src/schemas/types/HodlType.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import {
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLString,
|
||||||
|
GraphQLInt,
|
||||||
|
GraphQLBoolean,
|
||||||
|
GraphQLList,
|
||||||
|
} from 'graphql';
|
||||||
|
|
||||||
|
export const HodlOfferFeeType = new GraphQLObjectType({
|
||||||
|
name: 'hodlOfferFeeType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
author_fee_rate: { type: GraphQLString },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HodlOfferPaymentType = new GraphQLObjectType({
|
||||||
|
name: 'hodlOfferPaymentType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
id: { type: GraphQLString },
|
||||||
|
version: { type: GraphQLString },
|
||||||
|
payment_method_id: { type: GraphQLString },
|
||||||
|
payment_method_type: { type: GraphQLString },
|
||||||
|
payment_method_name: { type: GraphQLString },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HodlOfferTraderType = new GraphQLObjectType({
|
||||||
|
name: 'hodlOfferTraderType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
login: { type: GraphQLString },
|
||||||
|
online_status: { type: GraphQLString },
|
||||||
|
rating: { type: GraphQLString },
|
||||||
|
trades_count: { type: GraphQLInt },
|
||||||
|
url: { type: GraphQLString },
|
||||||
|
verified: { type: GraphQLBoolean },
|
||||||
|
verified_by: { type: GraphQLString },
|
||||||
|
strong_hodler: { type: GraphQLBoolean },
|
||||||
|
country: { type: GraphQLString },
|
||||||
|
country_code: { type: GraphQLString },
|
||||||
|
average_payment_time_minutes: { type: GraphQLInt },
|
||||||
|
average_release_time_minutes: { type: GraphQLInt },
|
||||||
|
days_since_last_trade: { type: GraphQLInt },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HodlOfferType = new GraphQLObjectType({
|
||||||
|
name: 'hodlOfferType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
id: { type: GraphQLString },
|
||||||
|
version: { type: GraphQLString },
|
||||||
|
asset_code: { type: GraphQLString },
|
||||||
|
searchable: { type: GraphQLBoolean },
|
||||||
|
country: { type: GraphQLString },
|
||||||
|
country_code: { type: GraphQLString },
|
||||||
|
working_now: { type: GraphQLBoolean },
|
||||||
|
side: { type: GraphQLString },
|
||||||
|
title: { type: GraphQLString },
|
||||||
|
description: { type: GraphQLString },
|
||||||
|
currency_code: { type: GraphQLString },
|
||||||
|
price: { type: GraphQLString },
|
||||||
|
min_amount: { type: GraphQLString },
|
||||||
|
max_amount: { type: GraphQLString },
|
||||||
|
first_trade_limit: { type: GraphQLString },
|
||||||
|
fee: { type: HodlOfferFeeType },
|
||||||
|
balance: { type: GraphQLString },
|
||||||
|
payment_window_minutes: { type: GraphQLInt },
|
||||||
|
confirmations: { type: GraphQLInt },
|
||||||
|
payment_method_instructions: {
|
||||||
|
type: new GraphQLList(HodlOfferPaymentType),
|
||||||
|
},
|
||||||
|
trader: { type: HodlOfferTraderType },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HodlCountryType = new GraphQLObjectType({
|
||||||
|
name: 'hodlCountryType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
code: { type: GraphQLString },
|
||||||
|
name: { type: GraphQLString },
|
||||||
|
native_name: { type: GraphQLString },
|
||||||
|
currency_code: { type: GraphQLString },
|
||||||
|
currency_name: { type: GraphQLString },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const HodlCurrencyType = new GraphQLObjectType({
|
||||||
|
name: 'hodlCurrencyType',
|
||||||
|
fields: () => {
|
||||||
|
return {
|
||||||
|
code: { type: GraphQLString },
|
||||||
|
name: { type: GraphQLString },
|
||||||
|
type: { type: GraphQLString },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
5
server/src/utils/appUrls.ts
Normal file
5
server/src/utils/appUrls.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const appUrls = {
|
||||||
|
fees: 'https://bitcoinfees.earn.com/api/v1/fees/recommended',
|
||||||
|
ticker: 'https://blockchain.info/ticker',
|
||||||
|
hodlhodl: 'https://hodlhodl.com/api',
|
||||||
|
};
|
8
server/src/utils/envConfig.ts
Normal file
8
server/src/utils/envConfig.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
export const envConfig = {
|
||||||
|
port: process.env.PORT || 3001,
|
||||||
|
env: process.env.NODE_ENV || 'development',
|
||||||
|
logLevel: process.env.LOG_LEVEL || 'silly',
|
||||||
|
hodlKey: process.env.HODL_KEY,
|
||||||
|
};
|
|
@ -47,4 +47,7 @@ export const RateConfig: RateConfigProps = {
|
||||||
signMessage: { max: 10, window: '1s' },
|
signMessage: { max: 10, window: '1s' },
|
||||||
verifyMessage: { max: 10, window: '1s' },
|
verifyMessage: { max: 10, window: '1s' },
|
||||||
getUtxos: { max: 10, window: '1s' },
|
getUtxos: { max: 10, window: '1s' },
|
||||||
|
getOffers: { max: 10, window: '1s' },
|
||||||
|
getCountries: { max: 10, window: '1s' },
|
||||||
|
getCurrencies: { max: 10, window: '1s' },
|
||||||
};
|
};
|
||||||
|
|
2
server/types/ln-service.d.ts
vendored
2
server/types/ln-service.d.ts
vendored
|
@ -1 +1 @@
|
||||||
declare module "ln-service";
|
declare module 'ln-service';
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
exclude: [path.resolve(__dirname, "node_modules")],
|
exclude: [path.resolve(__dirname, 'node_modules')],
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
use: "ts-loader"
|
use: 'ts-loader',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: "server.js",
|
filename: 'server.js',
|
||||||
path: path.resolve(__dirname, "dist")
|
path: path.resolve(__dirname, 'dist'),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"]
|
extensions: ['.ts', '.js'],
|
||||||
},
|
},
|
||||||
target: "node"
|
target: 'node',
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
const merge = require("webpack-merge");
|
const merge = require('webpack-merge');
|
||||||
const nodeExternals = require("webpack-node-externals");
|
const nodeExternals = require('webpack-node-externals');
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
const webpack = require("webpack");
|
const webpack = require('webpack');
|
||||||
|
const WebpackShellPlugin = require('webpack-shell-plugin');
|
||||||
|
|
||||||
const common = require("./webpack.common.js");
|
const common = require('./webpack.common.js');
|
||||||
|
|
||||||
module.exports = merge.smart(common, {
|
module.exports = merge.smart(common, {
|
||||||
devtool: "inline-source-map",
|
devtool: 'inline-source-map',
|
||||||
entry: ["webpack/hot/poll?1000", path.join(__dirname, "src/main.ts")],
|
entry: ['webpack/hot/poll?1000', path.join(__dirname, 'src/main.ts')],
|
||||||
externals: [
|
externals: [
|
||||||
nodeExternals({
|
nodeExternals({
|
||||||
whitelist: ["webpack/hot/poll?1000"]
|
modulesDir: path.resolve(__dirname, '../node_modules'),
|
||||||
})
|
whitelist: ['webpack/hot/poll?1000'],
|
||||||
|
}),
|
||||||
|
nodeExternals({
|
||||||
|
whitelist: ['webpack/hot/poll?1000'],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
mode: "development",
|
mode: 'development',
|
||||||
plugins: [new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin()],
|
plugins: [
|
||||||
watch: true
|
new CleanWebpackPlugin(),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new WebpackShellPlugin({
|
||||||
|
onBuildEnd: ['yarn dev'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
watch: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,12 @@ const common = require('./webpack.common.js');
|
||||||
module.exports = merge(common, {
|
module.exports = merge(common, {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
entry: [path.join(__dirname, 'src/main.ts')],
|
entry: [path.join(__dirname, 'src/main.ts')],
|
||||||
externals: [nodeExternals({})],
|
externals: [
|
||||||
|
nodeExternals({
|
||||||
|
modulesDir: path.resolve(__dirname, '../node_modules'),
|
||||||
|
}),
|
||||||
|
nodeExternals({}),
|
||||||
|
],
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
plugins: [new CleanWebpackPlugin()],
|
plugins: [new CleanWebpackPlugin()],
|
||||||
});
|
});
|
||||||
|
|
5818
server/yarn.lock
5818
server/yarn.lock
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue