mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-03-11 01:27:30 +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**
|
||||
|
||||

|
||||
[](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
|
||||
|
||||
|
@ -16,6 +16,8 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
|||
|
||||
### Tech Stack
|
||||
|
||||
The repository consists of two packages (client and server) and is maintained with LernaJS and Yarn Workspaces.
|
||||
|
||||
#### Client
|
||||
|
||||
[](https://snyk.io/test/github/apotdevin/thunderhub)
|
||||
|
@ -48,6 +50,7 @@ ThunderHub is an **open-source** LND node manager where you can manage and monit
|
|||
- View all transactions.
|
||||
- View all forwarded payments.
|
||||
- View all chain transactions.
|
||||
- View all unspent UTXOS.
|
||||
|
||||
### 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))
|
||||
- Update your all your channels fees or individual ones.
|
||||
- Backup, verify and recover all your channels.
|
||||
- Sign and verify messages.
|
||||
|
||||
### 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**.
|
||||
- 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.
|
||||
|
||||
### Deployment
|
||||
|
||||
- Docker images for easier deployment (WIP)
|
||||
|
||||
### Future Features
|
||||
|
||||
- Loop In and Out to provide liquidity or remove it from your channels.
|
||||
- Docker Image for easy deployments.
|
||||
- Integration with HodlHodl
|
||||
- Storefront interface
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -88,26 +97,19 @@ git clone https://github.com/apotdevin/thunderhub.git
|
|||
### **Requirements**
|
||||
|
||||
- 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**
|
||||
|
||||
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
|
||||
|
||||
##### This must be done in the `/server` folder
|
||||
|
||||
```javascript
|
||||
//With yarn:
|
||||
yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
```javascript
|
||||
//With npm:
|
||||
npm run build
|
||||
npm run start
|
||||
yarn server:prod
|
||||
yarn server:run
|
||||
```
|
||||
|
||||
If the server starts succesfully, you should see `info [server.js]: Server ready at http://localhost:3001/` in the terminal
|
||||
|
@ -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
|
||||
|
||||
```javascript
|
||||
//With yarn:
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
```javascript
|
||||
//With yarn:
|
||||
yarn build:dev
|
||||
|
||||
// In another terminal
|
||||
yarn dev
|
||||
```
|
||||
|
||||
```javascript
|
||||
//With npm:
|
||||
npm run build:dev
|
||||
|
||||
// In another terminal
|
||||
npm run dev
|
||||
yarn server:dev
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```javascript
|
||||
//With yarn:
|
||||
yarn storybook
|
||||
```
|
||||
|
||||
```javascript
|
||||
//With npm:
|
||||
npm run storybook
|
||||
```
|
||||
|
|
|
@ -7,7 +7,7 @@ module.exports = {
|
|||
'@storybook/addon-links',
|
||||
'@storybook/addon-viewport/register',
|
||||
],
|
||||
webpackFinal: async config => {
|
||||
webpackFinal: async (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
use: [
|
||||
|
|
|
@ -17,7 +17,7 @@ const StyledBackground = styled.div`
|
|||
align-items: center;
|
||||
`;
|
||||
|
||||
const ThemeDecorator = storyFn => {
|
||||
const ThemeDecorator = (storyFn) => {
|
||||
const background = boolean('No Background', false);
|
||||
const cardBackground = boolean('Card Background', true);
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "thunderhub-client",
|
||||
"version": "0.1.17",
|
||||
"name": "@thunderhub/client",
|
||||
"version": "0.2.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -9,7 +9,8 @@
|
|||
"eject": "react-scripts eject",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"build-storybook": "build-storybook -s public",
|
||||
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client"
|
||||
"deploy": "yarn build && aws s3 --profile EBFullAccess sync build/ s3://thunderhub-client",
|
||||
"precommit": "pretty-quick --staged"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -21,14 +22,14 @@
|
|||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@types/crypto-js": "^3.1.44",
|
||||
"@types/jest": "25.1.4",
|
||||
"@types/jest": "25.1.5",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
"@types/lodash.sortby": "^4.7.6",
|
||||
"@types/node": "13.9.8",
|
||||
"@types/node": "13.11.0",
|
||||
"@types/numeral": "^0.0.26",
|
||||
"@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-dom": "16.9.6",
|
||||
"@types/react-modal": "^3.10.5",
|
||||
|
@ -42,10 +43,7 @@
|
|||
"@types/victory": "^33.1.4",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"apollo-boost": "^0.4.4",
|
||||
"base64url": "^3.0.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"date-fns": "^2.11.0",
|
||||
"graphql": "^14.6.0",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
@ -53,6 +51,7 @@
|
|||
"node-sass": "^4.13.0",
|
||||
"numeral": "^2.0.6",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"qs": "^6.9.3",
|
||||
"react": "^16.13.0",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.13.0",
|
||||
|
@ -83,9 +82,6 @@
|
|||
"@storybook/preset-create-react-app": "^2.1.1",
|
||||
"@storybook/react": "^5.3.18",
|
||||
"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"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
@ -102,10 +98,5 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
|
|||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
|
|
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 { getValue } from '../../helpers/Helpers';
|
||||
import { useSettings } from '../../context/SettingsContext';
|
||||
import { usePriceState } from 'context/PriceContext';
|
||||
import { usePriceState } from '../../context/PriceContext';
|
||||
|
||||
type PriceProps = {
|
||||
price: number;
|
||||
|
@ -43,7 +43,7 @@ export const AnimatedNumber = ({ amount }: AnimatedProps) => {
|
|||
|
||||
return (
|
||||
<animated.div>
|
||||
{value.interpolate(amount => getValue({ amount, ...priceProps }))}
|
||||
{value.interpolate((amount) => getValue({ amount, ...priceProps }))}
|
||||
</animated.div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ export const PasswordInput = ({
|
|||
<SubTitle>Please Input a Password</SubTitle>
|
||||
<Line>
|
||||
<Sub4Title>Password:</Sub4Title>
|
||||
<Input onChange={e => setPass(e.target.value)} />
|
||||
<Input onChange={(e) => setPass(e.target.value)} />
|
||||
</Line>
|
||||
<Line>
|
||||
<Sub4Title>Strength:</Sub4Title>
|
||||
|
|
|
@ -12,9 +12,10 @@ import styled from 'styled-components';
|
|||
import { useAccount } from '../../../context/AccountContext';
|
||||
import { saveSessionAuth } from '../../../utils/auth';
|
||||
import { useSettings } from '../../../context/SettingsContext';
|
||||
import { textColorMap } from '../../../styles/Themes';
|
||||
import { textColorMap, mediaDimensions } from '../../../styles/Themes';
|
||||
import { ColorButton } from '../colorButton/ColorButton';
|
||||
import { Input } from '../../input/Input';
|
||||
import { useSize } from 'hooks/UseSize';
|
||||
|
||||
const RadioText = styled.div`
|
||||
margin-left: 10px;
|
||||
|
@ -42,7 +43,9 @@ export const LoginModal = ({
|
|||
callback,
|
||||
variables,
|
||||
}: LoginProps) => {
|
||||
const { width } = useSize();
|
||||
const { theme } = useSettings();
|
||||
|
||||
const [pass, setPass] = useState<string>('');
|
||||
const [storeSession, setStoreSession] = useState<boolean>(false);
|
||||
const { host, cert, refreshAccount } = useAccount();
|
||||
|
@ -83,7 +86,13 @@ export const LoginModal = ({
|
|||
<SubTitle>Unlock your Account</SubTitle>
|
||||
<ResponsiveLine>
|
||||
<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>
|
||||
<NoWrapTitle>Don't ask me again this session:</NoWrapTitle>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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 { formatDistanceStrict, format } from 'date-fns';
|
||||
import { XSvg } from './Icons';
|
||||
|
||||
export const getTransactionLink = (transaction: string) => {
|
||||
const link = `https://www.blockchain.com/btc/tx/${transaction}`;
|
||||
|
@ -51,12 +52,23 @@ export const renderLine = (
|
|||
title: string,
|
||||
content: any,
|
||||
key?: string | number,
|
||||
deleteCallback?: () => void,
|
||||
) => {
|
||||
if (!content) return null;
|
||||
return (
|
||||
<DetailLine key={key}>
|
||||
<DarkSubTitle>{title}</DarkSubTitle>
|
||||
<OverflowText>{content}</OverflowText>
|
||||
<SingleLine>
|
||||
<OverflowText>{content}</OverflowText>
|
||||
{deleteCallback && (
|
||||
<div
|
||||
style={{ margin: '0 0 -4px 4px' }}
|
||||
onClick={deleteCallback}
|
||||
>
|
||||
<XSvg />
|
||||
</div>
|
||||
)}
|
||||
</SingleLine>
|
||||
</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 Repeat } from '../../assets/icons/repeat.svg';
|
||||
import { ReactComponent as CheckIcon } from '../../assets/icons/check.svg';
|
||||
import { ReactComponent as StarIcon } from '../../assets/icons/star.svg';
|
||||
import { ReactComponent as HalfStarIcon } from '../../assets/icons/half-star.svg';
|
||||
import { ReactComponent as CreditCardIcon } from '../../assets/icons/credit-card.svg';
|
||||
|
||||
export interface IconProps {
|
||||
color?: string;
|
||||
|
@ -116,3 +119,6 @@ export const MailIcon = styleIcon(Mail);
|
|||
export const GithubIcon = styleIcon(Github);
|
||||
export const RepeatIcon = styleIcon(Repeat);
|
||||
export const Check = styleIcon(CheckIcon);
|
||||
export const Star = styleIcon(StarIcon);
|
||||
export const HalfStar = styleIcon(HalfStarIcon);
|
||||
export const CreditCard = styleIcon(CreditCardIcon);
|
||||
|
|
|
@ -56,10 +56,11 @@ export const Separation = styled.div`
|
|||
interface SubCardProps {
|
||||
color?: string;
|
||||
padding?: string;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
export const SubCard = styled.div`
|
||||
margin-bottom: 10px;
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
|
||||
padding: ${({ padding }) => (padding ? padding : '16px')};
|
||||
background: ${subCardColor};
|
||||
border: 1px solid ${cardBorderColor};
|
||||
|
|
|
@ -76,7 +76,7 @@ export const Input = ({
|
|||
value={value}
|
||||
color={color}
|
||||
withMargin={withMargin}
|
||||
onChange={e => onChange(e)}
|
||||
onChange={(e) => onChange(e)}
|
||||
fullWidth={fullWidth}
|
||||
inputWidth={width}
|
||||
maxWidth={maxWidth}
|
||||
|
|
|
@ -8,12 +8,18 @@ interface StyledProps {
|
|||
fontColor?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const styledCss = css`
|
||||
color: ${({ fontColor, inheritColor }: StyledProps) =>
|
||||
inheritColor ? 'inherit' : fontColor ?? textColor};
|
||||
text-decoration: none;
|
||||
${({ fullWidth }: StyledProps) =>
|
||||
fullWidth &&
|
||||
css`
|
||||
width: 100%;
|
||||
`};
|
||||
|
||||
:hover {
|
||||
background: linear-gradient(
|
||||
|
@ -27,9 +33,11 @@ const styledCss = css`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(({ inheritColor, fontColor, underline, ...rest }) => (
|
||||
<RouterLink {...rest} />
|
||||
))(() => styledCss);
|
||||
const StyledLink = styled(
|
||||
({ inheritColor, fontColor, underline, fullWidth, ...rest }) => (
|
||||
<RouterLink {...rest} />
|
||||
),
|
||||
)(() => styledCss);
|
||||
|
||||
const StyledALink = styled.a`
|
||||
${styledCss}
|
||||
|
@ -42,6 +50,7 @@ interface LinkProps {
|
|||
color?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const Link = ({
|
||||
|
@ -51,8 +60,9 @@ export const Link = ({
|
|||
color,
|
||||
underline,
|
||||
inheritColor,
|
||||
fullWidth,
|
||||
}: LinkProps) => {
|
||||
const props = { fontColor: color, underline, inheritColor };
|
||||
const props = { fontColor: color, underline, inheritColor, fullWidth };
|
||||
|
||||
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 { ToolsView } from 'views/tools';
|
||||
import { ChainView } from 'views/chain/ChainView';
|
||||
import { TraderView } from 'views/trader/TraderView';
|
||||
|
||||
const Container = styled.div`
|
||||
display: grid;
|
||||
|
@ -79,6 +80,7 @@ const Content = () => {
|
|||
/>
|
||||
<Route path="/settings" render={() => getGrid(SettingsView)} />
|
||||
<Route path="/fees" render={() => getGrid(FeesView)} />
|
||||
<Route path="/trading" render={() => getGrid(TraderView)} />
|
||||
<Route path="/terms" render={() => <TermsView />} />
|
||||
<Route path="/privacy" render={() => <PrivacyView />} />
|
||||
<Route path="/faq" render={() => <FaqView />} />
|
||||
|
|
|
@ -85,7 +85,7 @@ export const Header = () => {
|
|||
const renderLoggedIn = () => {
|
||||
if (width <= mediaDimensions.mobile) {
|
||||
return (
|
||||
<IconWrapper onClick={() => setOpen(prev => !prev)}>
|
||||
<IconWrapper onClick={() => setOpen((prev) => !prev)}>
|
||||
{transitions.map(({ item, key, props }) =>
|
||||
item ? (
|
||||
<AnimatedClose
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
LinkIcon,
|
||||
RepeatIcon,
|
||||
Users,
|
||||
CreditCard,
|
||||
} from '../../components/generic/Icons';
|
||||
import { useSettings } from '../../context/SettingsContext';
|
||||
import { useConnectionState } from 'context/ConnectionContext';
|
||||
|
@ -59,6 +60,7 @@ const ButtonSection = styled.div`
|
|||
|
||||
const NavSeparation = styled.div`
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
interface NavProps {
|
||||
|
@ -124,6 +126,7 @@ const CHAIN_TRANS = '/chainTransactions';
|
|||
const TOOLS = '/tools';
|
||||
const SETTINGS = '/settings';
|
||||
const FEES = '/fees';
|
||||
const TRADER = '/trading';
|
||||
|
||||
interface NavigationProps {
|
||||
isBurger?: boolean;
|
||||
|
@ -169,6 +172,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
{renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)}
|
||||
{renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)}
|
||||
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
|
||||
{renderNavButton('P2P Trading', TRADER, CreditCard, sidebar)}
|
||||
{renderNavButton('Settings', SETTINGS, Settings, sidebar)}
|
||||
</ButtonSection>
|
||||
);
|
||||
|
@ -184,6 +188,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
{renderBurgerNav('Forwards', FORWARDS, GitPullRequest)}
|
||||
{renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)}
|
||||
{renderBurgerNav('Tools', TOOLS, Shield)}
|
||||
{renderBurgerNav('Trading', TRADER, CreditCard)}
|
||||
{renderBurgerNav('Settings', SETTINGS, Settings)}
|
||||
</BurgerRow>
|
||||
);
|
||||
|
|
|
@ -11,133 +11,134 @@
|
|||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
);
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
||||
window.location.href
|
||||
);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
||||
window.location.href,
|
||||
);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null &&
|
||||
contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
|
|||
import { ApolloError } from 'apollo-boost';
|
||||
|
||||
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) => {
|
||||
try {
|
||||
|
|
|
@ -42,7 +42,7 @@ export const BalanceRoute = ({
|
|||
}: BalancedRouteProps) => {
|
||||
const [getRoute, { loading, data, called }] = useLazyQuery(GET_ROUTES, {
|
||||
fetchPolicy: 'no-cache',
|
||||
onError: error => {
|
||||
onError: (error) => {
|
||||
callback();
|
||||
toast.error(getErrorContent(error));
|
||||
},
|
||||
|
@ -52,7 +52,7 @@ export const BalanceRoute = ({
|
|||
incoming && outgoing && amount && data && data.getRoutes && blocked;
|
||||
|
||||
const [payRoute, { loading: loadingP }] = useMutation(PAY_VIA_ROUTE, {
|
||||
onError: error => {
|
||||
onError: (error) => {
|
||||
callback();
|
||||
toast.error(getErrorContent(error));
|
||||
},
|
||||
|
|
|
@ -38,8 +38,9 @@ export const SessionLogin = () => {
|
|||
<SingleLine>
|
||||
<Sub4Title>Password:</Sub4Title>
|
||||
<Input
|
||||
type={'password'}
|
||||
withMargin={'0 0 0 16px'}
|
||||
onChange={e => setPass(e.target.value)}
|
||||
onChange={(e) => setPass(e.target.value)}
|
||||
/>
|
||||
</SingleLine>
|
||||
{pass !== '' && (
|
||||
|
|
|
@ -48,8 +48,8 @@ export const FeeCard = ({
|
|||
} = channelInfo;
|
||||
|
||||
const [updateFees] = useMutation(UPDATE_FEES, {
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
onCompleted: data => {
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
onCompleted: (data) => {
|
||||
setIndexOpen(0);
|
||||
data.updateFees
|
||||
? toast.success('Channel fees updated')
|
||||
|
@ -82,7 +82,9 @@ export const FeeCard = ({
|
|||
placeholder={'Sats'}
|
||||
color={textColorMap[theme]}
|
||||
type={textColorMap[theme]}
|
||||
onChange={e => setBaseFee(parseInt(e.target.value))}
|
||||
onChange={(e) =>
|
||||
setBaseFee(parseInt(e.target.value))
|
||||
}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<ResponsiveLine>
|
||||
|
@ -93,7 +95,9 @@ export const FeeCard = ({
|
|||
placeholder={'Sats/Million'}
|
||||
color={textColorMap[theme]}
|
||||
type={'number'}
|
||||
onChange={e => setFeeRate(parseInt(e.target.value))}
|
||||
onChange={(e) =>
|
||||
setFeeRate(parseInt(e.target.value))
|
||||
}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<SecureButton
|
||||
|
|
|
@ -56,7 +56,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
|
|||
const [request, setRequest] = useState('');
|
||||
|
||||
const [createInvoice, { data, loading }] = useMutation(CREATE_INVOICE, {
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -104,7 +104,7 @@ export const CreateInvoiceCard = ({ color }: { color: string }) => {
|
|||
}
|
||||
color={color}
|
||||
type={'number'}
|
||||
onChange={e => setAmount(parseInt(e.target.value))}
|
||||
onChange={(e) => setAmount(parseInt(e.target.value))}
|
||||
/>
|
||||
<SecureButton
|
||||
callback={createInvoice}
|
||||
|
|
|
@ -36,7 +36,7 @@ export const ReceiveOnChainCard = () => {
|
|||
const [received, setReceived] = useState(false);
|
||||
|
||||
const [createAddress, { data, loading }] = useMutation(CREATE_ADDRESS, {
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -63,7 +63,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
|||
const { fast, halfHour, hour } = useBitcoinState();
|
||||
|
||||
const [payAddress, { loading }] = useMutation(PAY_ADDRESS, {
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
onCompleted: () => {
|
||||
toast.success('Payment Sent!');
|
||||
setOpen();
|
||||
|
@ -113,7 +113,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
|||
withMargin={
|
||||
width <= mediaDimensions.mobile ? '' : '0 0 0 24px'
|
||||
}
|
||||
onChange={e => setAddress(e.target.value)}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<Separation />
|
||||
|
@ -140,7 +140,7 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
|||
placeholder={'Sats'}
|
||||
withMargin={'0 0 0 8px'}
|
||||
type={'number'}
|
||||
onChange={e => setTokens(parseInt(e.target.value))}
|
||||
onChange={(e) => setTokens(parseInt(e.target.value))}
|
||||
/>
|
||||
</SingleLine>
|
||||
)}
|
||||
|
@ -193,7 +193,9 @@ export const SendOnChainCard = ({ setOpen }: { setOpen: () => void }) => {
|
|||
}
|
||||
type={'number'}
|
||||
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 [openChannel] = useMutation(OPEN_CHANNEL, {
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
onError: (error) => toast.error(getErrorContent(error)),
|
||||
onCompleted: () => {
|
||||
toast.success('Channel Opened');
|
||||
setOpenCard('none');
|
||||
|
@ -82,7 +82,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
|||
withMargin={
|
||||
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
||||
}
|
||||
onChange={e => setPublicKey(e.target.value)}
|
||||
onChange={(e) => setPublicKey(e.target.value)}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<ResponsiveLine>
|
||||
|
@ -101,7 +101,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
|||
width <= mediaDimensions.mobile ? '' : '0 0 0 8px'
|
||||
}
|
||||
type={'number'}
|
||||
onChange={e => setSize(parseInt(e.target.value))}
|
||||
onChange={(e) => setSize(parseInt(e.target.value))}
|
||||
/>
|
||||
</ResponsiveLine>
|
||||
<Separation />
|
||||
|
@ -152,7 +152,7 @@ export const OpenChannelCard = ({ color, setOpenCard }: OpenChannelProps) => {
|
|||
placeholder={'Sats/Byte'}
|
||||
color={color}
|
||||
type={'number'}
|
||||
onChange={e => setFee(parseInt(e.target.value))}
|
||||
onChange={(e) => setFee(parseInt(e.target.value))}
|
||||
/>
|
||||
// </MultiButton>
|
||||
)}
|
||||
|
|
|
@ -96,7 +96,7 @@ Props) => {
|
|||
grid: { stroke: chartGridColor[theme] },
|
||||
axis: { stroke: 'transparent' },
|
||||
}}
|
||||
tickFormat={a =>
|
||||
tickFormat={(a) =>
|
||||
isType === 'tokens' ? format({ amount: a }) : a
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -21,10 +21,10 @@ export const getWaterfall = (
|
|||
|
||||
for (let i = initialPeriod; i <= lastPeriod; i++) {
|
||||
const currentInvoice = invoices.find(
|
||||
invoice => invoice.period === i,
|
||||
(invoice) => invoice.period === i,
|
||||
) ?? { period: undefined, amount: 0, tokens: 0 };
|
||||
const currentPayment = payments.find(
|
||||
payment => payment.period === i,
|
||||
(payment) => payment.period === i,
|
||||
) ?? { period: undefined, amount: 0, tokens: 0 };
|
||||
|
||||
const amountChange = currentInvoice.amount - currentPayment.amount;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { Card } from "../../components/generic/Styled";
|
||||
import React from 'react';
|
||||
import { Card } from '../../components/generic/Styled';
|
||||
|
||||
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 { PaymentsCard } from './PaymentsCards';
|
||||
import { LoadingCard } from '../../components/loading/LoadingCard';
|
||||
import { useSettings } from '../../context/SettingsContext';
|
||||
import { textColorMap } from '../../styles/Themes';
|
||||
import { ColorButton } from 'components/buttons/colorButton/ColorButton';
|
||||
import { FlowBox } from 'views/home/reports/flow';
|
||||
|
||||
|
@ -18,7 +16,6 @@ export const TransactionList = () => {
|
|||
const [token, setToken] = useState('');
|
||||
const [fetching, setFetching] = useState(false);
|
||||
|
||||
const { theme } = useSettings();
|
||||
const { host, viewOnly, cert, sessionAdmin } = useAccount();
|
||||
const auth = {
|
||||
host,
|
||||
|
@ -48,7 +45,7 @@ export const TransactionList = () => {
|
|||
<FlowBox />
|
||||
<CardWithTitle>
|
||||
<SubTitle>Transactions</SubTitle>
|
||||
<Card bottom={'5px'}>
|
||||
<Card bottom={'8px'}>
|
||||
{resumeList.map((entry: any, index: number) => {
|
||||
if (entry.type === 'invoice') {
|
||||
return (
|
||||
|
@ -74,9 +71,7 @@ export const TransactionList = () => {
|
|||
})}
|
||||
</Card>
|
||||
<ColorButton
|
||||
selected={true}
|
||||
loading={fetching}
|
||||
color={textColorMap[theme]}
|
||||
disabled={fetching}
|
||||
onClick={() => {
|
||||
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,4 +1,4 @@
|
|||
{
|
||||
"ignore": ["src"],
|
||||
"watch": ["dist/"]
|
||||
"ignore": ["src"],
|
||||
"watch": ["dist/"]
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"name": "thunderhub-server",
|
||||
"version": "0.1.17",
|
||||
"name": "@thunderhub/server",
|
||||
"version": "0.2.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --config webpack.production.js",
|
||||
"build:dev": "webpack --config webpack.development.js",
|
||||
"start": "node dist/server",
|
||||
"dev": "nodemon dist/server",
|
||||
"deploy": "yarn build && eb deploy"
|
||||
"start": "node --http-parser=legacy dist/server",
|
||||
"dev": "nodemon --http-parser=legacy dist/server",
|
||||
"deploy": "yarn build && eb deploy",
|
||||
"precommit": "pretty-quick --staged"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -23,10 +24,7 @@
|
|||
"@types/node-fetch": "^2.5.5",
|
||||
"@types/underscore": "^1.9.4",
|
||||
"apollo-server": "^2.11.0",
|
||||
"base64url": "^3.0.1",
|
||||
"date-fns": "^2.11.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-depth-limit": "^1.1.0",
|
||||
"graphql-iso-date": "^3.6.1",
|
||||
"graphql-rate-limit": "^2.0.1",
|
||||
|
@ -37,19 +35,12 @@
|
|||
"devDependencies": {
|
||||
"@types/webpack-env": "^1.15.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"husky": "^4.2.3",
|
||||
"prettier": "^2.0.2",
|
||||
"pretty-quick": "^2.0.0",
|
||||
"ts-loader": "^6.2.2",
|
||||
"typescript": "^3.8.3",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"webpack-shell-plugin": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import base64url from 'base64url';
|
||||
import { authenticatedLndGrpc } from 'ln-service';
|
||||
import { envConfig } from '../utils/envConfig';
|
||||
|
||||
export const getIp = (req: any) => {
|
||||
if (!req || !req.headers) {
|
||||
|
@ -9,7 +9,7 @@ export const getIp = (req: any) => {
|
|||
const before = forwarded
|
||||
? forwarded.split(/, /)[0]
|
||||
: 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;
|
||||
};
|
||||
|
||||
|
|
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 path from "path";
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
import path from 'path';
|
||||
import { envConfig } from '../utils/envConfig';
|
||||
|
||||
const combinedFormat =
|
||||
process.env.NODE_ENV === "development"
|
||||
? format.combine(
|
||||
format.label({
|
||||
label: path.basename(
|
||||
process && process.mainModule ? process.mainModule.filename : ""
|
||||
envConfig.env === 'development'
|
||||
? format.combine(
|
||||
format.label({
|
||||
label: path.basename(
|
||||
process && process.mainModule
|
||||
? process.mainModule.filename
|
||||
: '',
|
||||
),
|
||||
}),
|
||||
format.splat(),
|
||||
format.colorize(),
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
format.printf(
|
||||
(info: any) =>
|
||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
|
||||
),
|
||||
)
|
||||
}),
|
||||
format.splat(),
|
||||
format.colorize(),
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(
|
||||
(info: any) =>
|
||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
|
||||
)
|
||||
)
|
||||
: format.combine(
|
||||
format.label({
|
||||
label: path.basename(
|
||||
process && process.mainModule ? process.mainModule.filename : ""
|
||||
)
|
||||
}),
|
||||
format.splat(),
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
format.printf(
|
||||
(info: any) =>
|
||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
|
||||
)
|
||||
);
|
||||
: format.combine(
|
||||
format.label({
|
||||
label: path.basename(
|
||||
process && process.mainModule
|
||||
? process.mainModule.filename
|
||||
: '',
|
||||
),
|
||||
}),
|
||||
format.splat(),
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
format.printf(
|
||||
(info: any) =>
|
||||
`${info.timestamp} ${info.level} [${info.label}]: ${info.message}`,
|
||||
),
|
||||
);
|
||||
|
||||
export const logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || "silly",
|
||||
format: combinedFormat,
|
||||
transports: [new transports.Console()]
|
||||
level: envConfig.logLevel,
|
||||
format: combinedFormat,
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { thunderHubSchema } from './schemas';
|
|||
import { logger } from './helpers/logger';
|
||||
import { getIp } from './helpers/helpers';
|
||||
import depthLimit from 'graphql-depth-limit';
|
||||
import { envConfig } from './utils/envConfig';
|
||||
|
||||
const server = new ApolloServer({
|
||||
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}`);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { GraphQLSchema, GraphQLObjectType } from "graphql";
|
||||
import { query } from "./query";
|
||||
import { mutation } from "./mutations";
|
||||
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
|
||||
import { query } from './query';
|
||||
import { mutation } from './mutations';
|
||||
|
||||
export const thunderHubSchema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: "Query",
|
||||
fields: query
|
||||
}),
|
||||
mutation: new GraphQLObjectType({
|
||||
name: "Mutation",
|
||||
fields: mutation
|
||||
})
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: query,
|
||||
}),
|
||||
mutation: new GraphQLObjectType({
|
||||
name: 'Mutation',
|
||||
fields: mutation,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -3,8 +3,7 @@ import { requestLimiter } from '../../../helpers/rateLimiter';
|
|||
import { GraphQLBoolean } from 'graphql';
|
||||
import fetch from 'node-fetch';
|
||||
import { BitcoinFeeType } from '../../types/QueryType';
|
||||
|
||||
const url = 'https://bitcoinfees.earn.com/api/v1/fees/recommended';
|
||||
import { appUrls } from '../../../utils/appUrls';
|
||||
|
||||
export const getBitcoinFees = {
|
||||
type: BitcoinFeeType,
|
||||
|
@ -15,7 +14,7 @@ export const getBitcoinFees = {
|
|||
await requestLimiter(context.ip, 'bitcoinFee');
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(appUrls.fees);
|
||||
const json = await response.json();
|
||||
|
||||
if (json) {
|
||||
|
|
|
@ -2,8 +2,7 @@ import { logger } from '../../../helpers/logger';
|
|||
import { requestLimiter } from '../../../helpers/rateLimiter';
|
||||
import { GraphQLString, GraphQLBoolean } from 'graphql';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const url = 'https://blockchain.info/ticker';
|
||||
import { appUrls } from '../../../utils/appUrls';
|
||||
|
||||
export const getBitcoinPrice = {
|
||||
type: GraphQLString,
|
||||
|
@ -17,7 +16,7 @@ export const getBitcoinPrice = {
|
|||
await requestLimiter(context.ip, 'bitcoinPrice');
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(appUrls.ticker);
|
||||
const json = await response.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 { messageQueries } from './message';
|
||||
import { chainQueries } from './chain';
|
||||
import { hodlQueries } from './hodlhodl';
|
||||
|
||||
export const query = {
|
||||
...channelQueries,
|
||||
|
@ -22,4 +23,5 @@ export const query = {
|
|||
...peerQueries,
|
||||
...messageQueries,
|
||||
...chainQueries,
|
||||
...hodlQueries,
|
||||
};
|
||||
|
|
|
@ -72,7 +72,7 @@ export const getForwardChannelsReport = {
|
|||
|
||||
const getRouteAlias = (array: any[], publicKey: string) =>
|
||||
Promise.all(
|
||||
array.map(async channel => {
|
||||
array.map(async (channel) => {
|
||||
const nodeAliasIn = await getNodeAlias(
|
||||
channel.in,
|
||||
publicKey,
|
||||
|
@ -94,7 +94,7 @@ export const getForwardChannelsReport = {
|
|||
|
||||
const getAlias = (array: any[], publicKey: string) =>
|
||||
Promise.all(
|
||||
array.map(async channel => {
|
||||
array.map(async (channel) => {
|
||||
const nodeAlias = await getNodeAlias(
|
||||
channel.name,
|
||||
publicKey,
|
||||
|
@ -120,7 +120,7 @@ export const getForwardChannelsReport = {
|
|||
});
|
||||
|
||||
if (params.type === 'route') {
|
||||
const mapped = forwardsList.forwards.map(forward => {
|
||||
const mapped = forwardsList.forwards.map((forward) => {
|
||||
return {
|
||||
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
|
||||
...forward,
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
export interface ForwardProps {
|
||||
created_at: string;
|
||||
fee: number;
|
||||
fee_mtokens: string;
|
||||
incoming_channel: string;
|
||||
mtokens: string;
|
||||
outgoing_channel: string;
|
||||
tokens: number;
|
||||
created_at: string;
|
||||
fee: number;
|
||||
fee_mtokens: string;
|
||||
incoming_channel: string;
|
||||
mtokens: string;
|
||||
outgoing_channel: string;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
export interface ForwardCompleteProps {
|
||||
forwards: ForwardProps[];
|
||||
next: string;
|
||||
forwards: ForwardProps[];
|
||||
next: string;
|
||||
}
|
||||
|
||||
export interface ListProps {
|
||||
[key: string]: ForwardProps[];
|
||||
[key: string]: ForwardProps[];
|
||||
}
|
||||
|
||||
export interface ReduceObjectProps {
|
||||
fee: number;
|
||||
tokens: number;
|
||||
fee: number;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
export interface FinalProps {
|
||||
fee: number;
|
||||
tokens: number;
|
||||
amount: number;
|
||||
fee: number;
|
||||
tokens: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface FinalList {
|
||||
[key: string]: FinalProps;
|
||||
[key: string]: FinalProps;
|
||||
}
|
||||
|
||||
export interface CountProps {
|
||||
[key: string]: number;
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export interface ChannelCounts {
|
||||
name: string;
|
||||
count: number;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ export const getForwardReport = {
|
|||
});
|
||||
|
||||
if (params.time === 'month' || params.time === 'week') {
|
||||
const orderedDay = groupBy(forwardsList.forwards, item => {
|
||||
const orderedDay = groupBy(forwardsList.forwards, (item) => {
|
||||
return (
|
||||
days -
|
||||
differenceInCalendarDays(
|
||||
|
@ -60,7 +60,7 @@ export const getForwardReport = {
|
|||
|
||||
return JSON.stringify(reducedOrderedDay);
|
||||
} else {
|
||||
const orderedHour = groupBy(forwardsList.forwards, item => {
|
||||
const orderedHour = groupBy(forwardsList.forwards, (item) => {
|
||||
return (
|
||||
24 -
|
||||
differenceInHours(endDate, new Date(item.created_at))
|
||||
|
|
|
@ -56,11 +56,11 @@ export const countArray = (list: ForwardProps[], type: boolean) => {
|
|||
const element = grouped[key];
|
||||
|
||||
const fee = element
|
||||
.map(forward => forward.fee)
|
||||
.map((forward) => forward.fee)
|
||||
.reduce((p, c) => p + c);
|
||||
|
||||
const tokens = element
|
||||
.map(forward => forward.tokens)
|
||||
.map((forward) => forward.tokens)
|
||||
.reduce((p, c) => p + c);
|
||||
|
||||
channelInfo.push({
|
||||
|
@ -84,11 +84,11 @@ export const countRoutes = (list: ForwardProps[]) => {
|
|||
const element = grouped[key];
|
||||
|
||||
const fee = element
|
||||
.map(forward => forward.fee)
|
||||
.map((forward) => forward.fee)
|
||||
.reduce((p, c) => p + c);
|
||||
|
||||
const tokens = element
|
||||
.map(forward => forward.tokens)
|
||||
.map((forward) => forward.tokens)
|
||||
.reduce((p, c) => p + c);
|
||||
|
||||
channelInfo.push({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getForwardReport } from "./ForwardReport";
|
||||
import { getForwardChannelsReport } from "./ForwardChannels";
|
||||
import { getForwardReport } from './ForwardReport';
|
||||
import { getForwardChannelsReport } from './ForwardChannels';
|
||||
|
||||
export const reportQueries = {
|
||||
getForwardReport,
|
||||
getForwardChannelsReport
|
||||
getForwardReport,
|
||||
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' },
|
||||
verifyMessage: { max: 10, window: '1s' },
|
||||
getUtxos: { max: 10, window: '1s' },
|
||||
getOffers: { max: 10, window: '1s' },
|
||||
getCountries: { max: 10, window: '1s' },
|
||||
getCurrencies: { max: 10, window: '1s' },
|
||||
};
|
||||
|
|
|
@ -1,66 +1,66 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6"
|
||||
] /* Specify library files to be included in the compilation. */,
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "dist" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
"removeComments": true /* Do not emit comments to output. */,
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6"
|
||||
] /* Specify library files to be included in the compilation. */,
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "dist" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
"removeComments": true /* Do not emit comments to output. */,
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
|
|
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: {
|
||||
rules: [
|
||||
{
|
||||
exclude: [path.resolve(__dirname, "node_modules")],
|
||||
test: /\.ts$/,
|
||||
use: "ts-loader"
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: "server.js",
|
||||
path: path.resolve(__dirname, "dist")
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"]
|
||||
},
|
||||
target: "node"
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
exclude: [path.resolve(__dirname, 'node_modules')],
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
filename: 'server.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
target: 'node',
|
||||
};
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const merge = require("webpack-merge");
|
||||
const nodeExternals = require("webpack-node-externals");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const merge = require('webpack-merge');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const path = require('path');
|
||||
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, {
|
||||
devtool: "inline-source-map",
|
||||
entry: ["webpack/hot/poll?1000", path.join(__dirname, "src/main.ts")],
|
||||
externals: [
|
||||
nodeExternals({
|
||||
whitelist: ["webpack/hot/poll?1000"]
|
||||
})
|
||||
],
|
||||
mode: "development",
|
||||
plugins: [new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin()],
|
||||
watch: true
|
||||
devtool: 'inline-source-map',
|
||||
entry: ['webpack/hot/poll?1000', path.join(__dirname, 'src/main.ts')],
|
||||
externals: [
|
||||
nodeExternals({
|
||||
modulesDir: path.resolve(__dirname, '../node_modules'),
|
||||
whitelist: ['webpack/hot/poll?1000'],
|
||||
}),
|
||||
nodeExternals({
|
||||
whitelist: ['webpack/hot/poll?1000'],
|
||||
}),
|
||||
],
|
||||
mode: 'development',
|
||||
plugins: [
|
||||
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, {
|
||||
devtool: 'source-map',
|
||||
entry: [path.join(__dirname, 'src/main.ts')],
|
||||
externals: [nodeExternals({})],
|
||||
externals: [
|
||||
nodeExternals({
|
||||
modulesDir: path.resolve(__dirname, '../node_modules'),
|
||||
}),
|
||||
nodeExternals({}),
|
||||
],
|
||||
mode: 'production',
|
||||
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