add exchange order screen

This commit is contained in:
João Geonizeli
2021-08-13 23:55:37 -03:00
parent be31d7165a
commit 4922994da6
12 changed files with 516 additions and 3 deletions

View File

@@ -39,6 +39,36 @@ type Currency implements Node {
name: String!
}
"""
The connection type for Currency.
"""
type CurrencyConnection {
"""
A list of edges.
"""
edges: [CurrencyEdge!]!
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type CurrencyEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Currency!
}
type FiatBalance implements Node {
amountCents: Int!
amountCurrency: String!
@@ -139,6 +169,27 @@ type Query {
"""
last: Int
): BalanceConnection!
currencies(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): CurrencyConnection!
currentUser: User
fiatBalances(
"""

View File

@@ -2,7 +2,7 @@ import type { FC } from "react";
import React from "react";
import { Switch, Route } from "react-router-dom";
import { Home, Wallet } from "./pages";
import { Home, Orders, Wallet } from "./pages";
export const Routes: FC = () => {
return (
@@ -13,6 +13,9 @@ export const Routes: FC = () => {
<Route exact path="/wallet">
<Wallet />
</Route>
<Route exact path="/orders/exchange">
<Orders.Exchange />
</Route>
</Switch>
);
};

View File

@@ -18,6 +18,10 @@ const MenuItems: MenuItem[] = [
label: "Carteira",
path: "/wallet",
},
{
label: "Ordem de Troca",
path: "/orders/exchange",
},
];
export const SideNav = () => {

View File

@@ -0,0 +1,182 @@
import React, { useState } from "react";
import type { FC } from "react";
import { graphql } from "babel-plugin-relay/macro";
import { useLazyLoadQuery } from "react-relay";
import { BigNumber } from "bignumber.js";
import cx from "classnames";
import { useCurrentUser } from "../../../contexts/UserProvider";
import { Unauthenticated } from "../../../messages/Unauthenticated";
import type { ExchangeQuery } from "./__generated__/ExchangeQuery.graphql";
const tabBaseStyles =
"w-full text-base font-bold text-black px-4 py-2 focus:ring-blue-500";
const selectedTabStyles =
"bg-blue-600 hover:bg-blue-700 rounded-l-frounded-full text-white";
const inputBaseStyles =
"rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent mb-3";
export const Exchange: FC = () => {
const { isAuthenticated } = useCurrentUser();
const [exchangeOption, setExchangeOption] = useState<"BUY" | "SELL">("BUY");
const [cryptoDock, setCryptoDock] = useState<string>("0");
const [fiatDock, setFiatDock] = useState<string>("0.00");
const { balances, fiatBalances } = useLazyLoadQuery<ExchangeQuery>(
graphql`
query ExchangeQuery {
fiatBalances {
edges {
node {
id
amountCents
amountCurrency
}
}
}
balances {
edges {
node {
id
amount
currency {
name
}
}
}
}
}
`,
{}
);
if (!isAuthenticated) return <Unauthenticated />;
const [crypto] = balances.edges;
const [fiat] = fiatBalances.edges;
const avaliableCrypto = new BigNumber(crypto.node.amount);
const avaliableFiat = (
fiat.node.amountCents ? fiat.node.amountCents / 100 : 0
).toFixed(2);
const handleSellTabClick = () => {
setExchangeOption("SELL");
setCryptoDock("0");
};
const handleBuyTabClick = () => {
setExchangeOption("BUY");
setCryptoDock("0");
};
const handleCryptoAmountChange = ({
currentTarget: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
const newCryptoAmount = new BigNumber(value);
if (newCryptoAmount.isLessThanOrEqualTo(avaliableCrypto)) {
setCryptoDock(value);
}
};
const handleFiatAmountChange = ({
currentTarget: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
const newFiatAmount = Number(value);
if (Number(avaliableFiat) >= newFiatAmount) {
setFiatDock(value);
}
};
const handleMaxFiatDockButton = () => {
setFiatDock(avaliableFiat);
};
const handleMaxCryptoButton = () => {
setCryptoDock(avaliableCrypto.toString());
};
return (
<div className="grid place-items-center w-full">
<div className="max-w-lg">
<div className="flex items-center bg-white rounded-full border border-gray-200 mb-3">
<button
type="button"
className={cx(
tabBaseStyles,
"rounded-full",
exchangeOption === "BUY" && selectedTabStyles
)}
onClick={handleBuyTabClick}
>
Comprar
</button>
<button
type="button"
className={cx(
tabBaseStyles,
"rounded-full",
exchangeOption === "SELL" && selectedTabStyles
)}
onClick={handleSellTabClick}
>
Vender
</button>
</div>
<form className="bg-white p-4 rounded-2xl border border-gray-200">
<span className="mb-2">
{exchangeOption === "SELL" ? "CAKE" : "BRL"} disponível:{" "}
{exchangeOption === "SELL" ? crypto.node.amount : avaliableFiat}
</span>
<div className="flex flex-row">
{exchangeOption === "BUY" ? (
<>
<input
className={cx(inputBaseStyles)}
type="number"
value={fiatDock}
onChange={handleFiatAmountChange}
/>
<button
type="button"
disabled={fiatDock === avaliableFiat}
className="flex items-center mb-3 ml-3 font-bold rounded-full text-red-500"
onClick={handleMaxFiatDockButton}
>
Max
</button>
</>
) : (
<>
<input
className={cx(inputBaseStyles)}
type="number"
value={cryptoDock}
onChange={handleCryptoAmountChange}
/>
<button
type="button"
disabled={avaliableCrypto.isEqualTo(cryptoDock)}
className="flex items-center mb-3 ml-3 font-bold rounded-full text-red-500"
onClick={handleMaxCryptoButton}
>
Max
</button>
</>
)}
</div>
<button
className="cursor-pointer py-2 px-4 bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-200 text-white w-full transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
type="submit"
>
{exchangeOption === "BUY" ? "Comprar" : "Vender"} CAKE
</button>
</form>
</div>
</div>
);
};

View File

@@ -0,0 +1,259 @@
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from "relay-runtime";
export type ExchangeQueryVariables = {};
export type ExchangeQueryResponse = {
readonly fiatBalances: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly amountCents: number;
readonly amountCurrency: string;
};
}>;
};
readonly balances: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly amount: string;
readonly currency: {
readonly name: string;
};
};
}>;
};
};
export type ExchangeQuery = {
readonly response: ExchangeQueryResponse;
readonly variables: ExchangeQueryVariables;
};
/*
query ExchangeQuery {
fiatBalances {
edges {
node {
id
amountCents
amountCurrency
}
}
}
balances {
edges {
node {
id
amount
currency {
name
id
}
}
}
}
}
*/
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"concreteType": "FiatBalanceConnection",
"kind": "LinkedField",
"name": "fiatBalances",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "FiatBalanceEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "FiatBalance",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "amountCents",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "amountCurrency",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "amount",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "ExchangeQuery",
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "BalanceConnection",
"kind": "LinkedField",
"name": "balances",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "BalanceEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Balance",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "ExchangeQuery",
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "BalanceConnection",
"kind": "LinkedField",
"name": "balances",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "BalanceEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Balance",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Currency",
"kind": "LinkedField",
"name": "currency",
"plural": false,
"selections": [
(v3/*: any*/),
(v0/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "7a42b2fc93f97505aaaf21867de36321",
"id": null,
"metadata": {},
"name": "ExchangeQuery",
"operationKind": "query",
"text": "query ExchangeQuery {\n fiatBalances {\n edges {\n node {\n id\n amountCents\n amountCurrency\n }\n }\n }\n balances {\n edges {\n node {\n id\n amount\n currency {\n name\n id\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = '80cac44f2e6288dfb573d0037b7d4148';
export default node;

View File

@@ -0,0 +1 @@
export * from "./Exchange";

View File

@@ -0,0 +1,5 @@
import { Exchange } from "./Exchange";
export const Orders = {
Exchange,
};

View File

@@ -1,2 +1,3 @@
export * from "./Home";
export * from "./Wallet";
export * from "./Orders";

View File

@@ -25,7 +25,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :validatable
has_many :documents, class_name: "UserDocument", dependent: :destroy
has_many :balances, dependent: :restrict_with_error
has_one :balance, dependent: :restrict_with_error
has_one :fiat_balance, dependent: :restrict_with_error
validates :first_name, :last_name, :email, presence: true

View File

@@ -22,6 +22,7 @@
"autoprefixer": "^9",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-relay": "^11.0.2",
"bignumber.js": "^9.0.1",
"classnames": "^2.3.1",
"postcss": "^7",
"ramda": "^0.27.1",

View File

@@ -31,6 +31,7 @@ RSpec.describe(User, type: :model) do
describe "associations" do
it { is_expected.to(have_many(:documents)) }
it { is_expected.to(have_many(:balances)) }
it { is_expected.to(have_one(:balance)) }
it { is_expected.to(have_one(:fiat_balance)) }
end
end

5
yarn.lock generated
View File

@@ -2024,6 +2024,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"