committed by
GitHub
9 changed files with 690 additions and 281 deletions
@ -1,42 +0,0 @@ |
|||||
.App { |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.App-logo { |
|
||||
height: 40vmin; |
|
||||
pointer-events: none; |
|
||||
} |
|
||||
|
|
||||
@media (prefers-reduced-motion: no-preference) { |
|
||||
.App-logo { |
|
||||
animation: App-logo-spin infinite 20s linear; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.App-header { |
|
||||
background-color: #282c34; |
|
||||
min-height: 100vh; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
font-size: calc(10px + 2vmin); |
|
||||
color: white; |
|
||||
} |
|
||||
|
|
||||
.App-link { |
|
||||
color: #61dafb; |
|
||||
} |
|
||||
|
|
||||
@keyframes App-logo-spin { |
|
||||
from { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
to { |
|
||||
transform: rotate(360deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
button { |
|
||||
font-size: calc(10px + 2vmin); |
|
||||
} |
|
@ -1,138 +0,0 @@ |
|||||
import { Box, Button, HStack, SimpleGrid, Text, useToast, VStack } from "@chakra-ui/react"; |
|
||||
import React from "react"; |
|
||||
import { useAsync } from "react-async"; |
|
||||
import { postAcceptOrder, postRejectOrder } from "../MakerClient"; |
|
||||
import { Cfd, unixTimestampToDate } from "./Types"; |
|
||||
|
|
||||
interface CfdTileProps { |
|
||||
index: number; |
|
||||
cfd: Cfd; |
|
||||
} |
|
||||
|
|
||||
export default function CfdTile( |
|
||||
{ |
|
||||
index, |
|
||||
cfd, |
|
||||
}: CfdTileProps, |
|
||||
) { |
|
||||
const toast = useToast(); |
|
||||
|
|
||||
let { run: acceptOrder, isLoading: isAccepting } = useAsync({ |
|
||||
deferFn: async ([args]: any[]) => { |
|
||||
try { |
|
||||
let payload = { |
|
||||
order_id: args.order_id, |
|
||||
}; |
|
||||
await postAcceptOrder(payload); |
|
||||
} catch (e) { |
|
||||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|
||||
|
|
||||
toast({ |
|
||||
title: "Error", |
|
||||
description, |
|
||||
status: "error", |
|
||||
duration: 9000, |
|
||||
isClosable: true, |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
let { run: rejectOrder, isLoading: isRejecting } = useAsync({ |
|
||||
deferFn: async ([args]: any[]) => { |
|
||||
try { |
|
||||
let payload = { |
|
||||
order_id: args.order_id, |
|
||||
}; |
|
||||
await postRejectOrder(payload); |
|
||||
} catch (e) { |
|
||||
const description = typeof e === "string" ? e : JSON.stringify(e); |
|
||||
|
|
||||
toast({ |
|
||||
title: "Error", |
|
||||
description, |
|
||||
status: "error", |
|
||||
duration: 9000, |
|
||||
isClosable: true, |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
let actionButtons; |
|
||||
if (cfd.state === "Open") { |
|
||||
actionButtons = <Box paddingBottom={5}> |
|
||||
<Button colorScheme="blue" variant="solid"> |
|
||||
Close |
|
||||
</Button> |
|
||||
</Box>; |
|
||||
} else if (cfd.state == "Requested") { |
|
||||
actionButtons = ( |
|
||||
<HStack> |
|
||||
<Box paddingBottom={5}> |
|
||||
<Button |
|
||||
colorScheme="blue" |
|
||||
variant="solid" |
|
||||
onClick={async () => acceptOrder(cfd)} |
|
||||
isLoading={isAccepting} |
|
||||
> |
|
||||
Accept |
|
||||
</Button> |
|
||||
</Box> |
|
||||
<Box paddingBottom={5}> |
|
||||
<Button |
|
||||
colorScheme="blue" |
|
||||
variant="solid" |
|
||||
onClick={async () => rejectOrder(cfd)} |
|
||||
isLoading={isRejecting} |
|
||||
> |
|
||||
Reject |
|
||||
</Button> |
|
||||
</Box> |
|
||||
</HStack> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Box borderRadius={"md"} borderColor={"blue.800"} borderWidth={2} bg={"gray.50"}> |
|
||||
<VStack> |
|
||||
<Box bg="blue.800" w="100%"> |
|
||||
<Text padding={2} color={"white"} fontWeight={"bold"}>CFD #{index}</Text> |
|
||||
</Box> |
|
||||
<SimpleGrid padding={5} columns={2} spacing={5}> |
|
||||
<Text>Trading Pair</Text> |
|
||||
<Text>{cfd.trading_pair}</Text> |
|
||||
<Text>Position</Text> |
|
||||
<Text>{cfd.position}</Text> |
|
||||
<Text>CFD Price</Text> |
|
||||
<Text>{cfd.initial_price}</Text> |
|
||||
<Text>Leverage</Text> |
|
||||
<Text>{cfd.leverage}</Text> |
|
||||
<Text>Quantity</Text> |
|
||||
<Text>{cfd.quantity_usd}</Text> |
|
||||
<Text>Margin</Text> |
|
||||
<Text>{cfd.margin}</Text> |
|
||||
<Text>Liquidation Price</Text> |
|
||||
<Text |
|
||||
overflow="hidden" |
|
||||
textOverflow="ellipsis" |
|
||||
whiteSpace="nowrap" |
|
||||
_hover={{ overflow: "visible" }} |
|
||||
> |
|
||||
{cfd.liquidation_price} |
|
||||
</Text> |
|
||||
<Text>Profit</Text> |
|
||||
<Text>{cfd.profit_usd}</Text> |
|
||||
<Text>Open since</Text> |
|
||||
{/* TODO: Format date in a more compact way */} |
|
||||
<Text> |
|
||||
{unixTimestampToDate(cfd.state_transition_timestamp).toString()} |
|
||||
</Text> |
|
||||
<Text>Status</Text> |
|
||||
<Text>{cfd.state}</Text> |
|
||||
</SimpleGrid> |
|
||||
{actionButtons} |
|
||||
</VStack> |
|
||||
</Box> |
|
||||
); |
|
||||
} |
|
@ -0,0 +1,271 @@ |
|||||
|
import { ChevronRightIcon, ChevronUpIcon, TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons"; |
||||
|
import { Badge, Box, chakra, HStack, IconButton, Table as CUITable, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; |
||||
|
import React from "react"; |
||||
|
import { Column, Row, useExpanded, useSortBy, useTable } from "react-table"; |
||||
|
import { Cfd } from "../Types"; |
||||
|
|
||||
|
interface CfdTableProps { |
||||
|
data: Cfd[]; |
||||
|
} |
||||
|
|
||||
|
export function CfdTable( |
||||
|
{ data }: CfdTableProps, |
||||
|
) { |
||||
|
const tableData = React.useMemo( |
||||
|
() => data, |
||||
|
[data], |
||||
|
); |
||||
|
|
||||
|
const columns: Array<Column<Cfd>> = React.useMemo( |
||||
|
() => [ |
||||
|
{ |
||||
|
id: "expander", |
||||
|
Header: () => null, |
||||
|
Cell: ({ row }: any) => ( |
||||
|
<span {...row.getToggleRowExpandedProps()}> |
||||
|
{row.isExpanded |
||||
|
? <IconButton |
||||
|
aria-label="Reduce" |
||||
|
icon={<ChevronUpIcon />} |
||||
|
onClick={() => { |
||||
|
row.toggleRowExpanded(); |
||||
|
}} |
||||
|
/> |
||||
|
: <IconButton |
||||
|
aria-label="Expand" |
||||
|
icon={<ChevronRightIcon />} |
||||
|
onClick={() => { |
||||
|
row.toggleRowExpanded(); |
||||
|
}} |
||||
|
/>} |
||||
|
</span> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
Header: "OrderId", |
||||
|
accessor: "order_id", // accessor is the "key" in the data
|
||||
|
}, |
||||
|
{ |
||||
|
Header: "Position", |
||||
|
accessor: ({ position }) => { |
||||
|
let colorScheme = "green"; |
||||
|
if (position.toLocaleLowerCase() === "buy") { |
||||
|
colorScheme = "purple"; |
||||
|
} |
||||
|
return ( |
||||
|
<Badge colorScheme={colorScheme}>{position}</Badge> |
||||
|
); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
Header: "Quantity", |
||||
|
accessor: ({ quantity_usd }) => { |
||||
|
return (<Dollars amount={quantity_usd} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Leverage", |
||||
|
accessor: "leverage", |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Margin", |
||||
|
accessor: "margin", |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Initial Price", |
||||
|
accessor: ({ initial_price }) => { |
||||
|
return (<Dollars amount={initial_price} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Liquidation Price", |
||||
|
isNumeric: true, |
||||
|
accessor: ({ liquidation_price }) => { |
||||
|
return (<Dollars amount={liquidation_price} />); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Unrealized P/L", |
||||
|
accessor: ({ profit_usd }) => { |
||||
|
return (<Dollars amount={profit_usd} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Timestamp", |
||||
|
accessor: "state_transition_timestamp", |
||||
|
}, |
||||
|
{ |
||||
|
Header: "State", |
||||
|
accessor: ({ state }) => { |
||||
|
let colorScheme = "gray"; |
||||
|
if (state.toLowerCase() === "rejected") { |
||||
|
colorScheme = "red"; |
||||
|
} |
||||
|
if (state.toLowerCase() === "contract setup") { |
||||
|
colorScheme = "green"; |
||||
|
} |
||||
|
return ( |
||||
|
<Badge colorScheme={colorScheme}>{state}</Badge> |
||||
|
); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
[], |
||||
|
); |
||||
|
|
||||
|
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
|
||||
|
const hiddenColumns = ["order_id", "leverage", "state_transition_timestamp"]; |
||||
|
|
||||
|
return ( |
||||
|
<Table |
||||
|
tableData={tableData} |
||||
|
columns={columns} |
||||
|
hiddenColumns={hiddenColumns} |
||||
|
renderDetails={renderRowSubComponent} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function renderRowSubComponent(row: Row<Cfd>) { |
||||
|
// TODO: I would show additional information here such as txids, timestamps, actions
|
||||
|
let cells = row.allCells |
||||
|
.filter((cell) => { |
||||
|
return ["state_transition_timestamp"].includes(cell.column.id); |
||||
|
}) |
||||
|
.map((cell) => { |
||||
|
return cell; |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
Showing some more information here... |
||||
|
<HStack> |
||||
|
{cells.map(cell => ( |
||||
|
<Box key={cell.column.id}> |
||||
|
{cell.column.id} = {cell.render("Cell")} |
||||
|
</Box> |
||||
|
))} |
||||
|
</HStack> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface DollarsProps { |
||||
|
amount: number; |
||||
|
} |
||||
|
function Dollars({ amount }: DollarsProps) { |
||||
|
const price = Math.floor(amount * 100.0) / 100.0; |
||||
|
return ( |
||||
|
<> |
||||
|
$ {price} |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface TableProps { |
||||
|
columns: Array<Column<Cfd>>; |
||||
|
tableData: Cfd[]; |
||||
|
hiddenColumns: string[]; |
||||
|
renderDetails: any; |
||||
|
} |
||||
|
|
||||
|
export function Table({ columns, tableData, hiddenColumns, renderDetails }: TableProps) { |
||||
|
const { |
||||
|
getTableProps, |
||||
|
getTableBodyProps, |
||||
|
headerGroups, |
||||
|
rows, |
||||
|
prepareRow, |
||||
|
visibleColumns, |
||||
|
} = useTable( |
||||
|
{ |
||||
|
columns, |
||||
|
data: tableData, |
||||
|
initialState: { |
||||
|
hiddenColumns, |
||||
|
}, |
||||
|
}, |
||||
|
useSortBy, |
||||
|
useExpanded, |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<CUITable {...getTableProps()} colorScheme="blue"> |
||||
|
<Thead> |
||||
|
{headerGroups.map((headerGroup) => ( |
||||
|
<Tr {...headerGroup.getHeaderGroupProps()}> |
||||
|
{headerGroup.headers.map((column) => ( |
||||
|
<Th |
||||
|
// @ts-ignore
|
||||
|
{...column.getHeaderProps(column.getSortByToggleProps())} |
||||
|
// @ts-ignore
|
||||
|
isNumeric={column.isNumeric} |
||||
|
> |
||||
|
{column.render("Header")} |
||||
|
<chakra.span pl="4"> |
||||
|
{// @ts-ignore
|
||||
|
column.isSorted |
||||
|
? ( |
||||
|
// @ts-ignore
|
||||
|
column.isSortedDesc |
||||
|
? ( |
||||
|
<TriangleDownIcon aria-label="sorted descending" /> |
||||
|
) |
||||
|
: ( |
||||
|
<TriangleUpIcon aria-label="sorted ascending" /> |
||||
|
) |
||||
|
) |
||||
|
: null} |
||||
|
</chakra.span> |
||||
|
</Th> |
||||
|
))} |
||||
|
</Tr> |
||||
|
))} |
||||
|
</Thead> |
||||
|
<Tbody {...getTableBodyProps()}> |
||||
|
{rows.map((row) => { |
||||
|
prepareRow(row); |
||||
|
return ( |
||||
|
<React.Fragment key={row.id}> |
||||
|
<Tr |
||||
|
{...row.getRowProps()} |
||||
|
onClick={() => |
||||
|
// @ts-ignore
|
||||
|
row.toggleRowExpanded()} |
||||
|
> |
||||
|
{row.cells.map((cell) => ( |
||||
|
// @ts-ignore
|
||||
|
<Td {...cell.getCellProps()} isNumeric={cell.column.isNumeric}> |
||||
|
{cell.render("Cell")} |
||||
|
</Td> |
||||
|
))} |
||||
|
</Tr> |
||||
|
|
||||
|
{// @ts-ignore
|
||||
|
row.isExpanded |
||||
|
? ( |
||||
|
<Tr> |
||||
|
<Td> |
||||
|
</Td> |
||||
|
<Td colSpan={visibleColumns.length - 1}> |
||||
|
{renderDetails(row)} |
||||
|
</Td> |
||||
|
</Tr> |
||||
|
) |
||||
|
: null} |
||||
|
</React.Fragment> |
||||
|
); |
||||
|
})} |
||||
|
</Tbody> |
||||
|
</CUITable> |
||||
|
</> |
||||
|
); |
||||
|
} |
@ -0,0 +1,236 @@ |
|||||
|
import { CheckIcon, ChevronRightIcon, ChevronUpIcon, CloseIcon } from "@chakra-ui/icons"; |
||||
|
import { Badge, Box, HStack, IconButton, useToast } from "@chakra-ui/react"; |
||||
|
import React from "react"; |
||||
|
import { useAsync } from "react-async"; |
||||
|
import { Column, Row } from "react-table"; |
||||
|
import { postAcceptOrder, postRejectOrder } from "../../MakerClient"; |
||||
|
import { Cfd } from "../Types"; |
||||
|
import { Table } from "./CfdTable"; |
||||
|
|
||||
|
interface CfdMableTakerProps { |
||||
|
data: Cfd[]; |
||||
|
} |
||||
|
|
||||
|
export function CfdTableMaker( |
||||
|
{ data }: CfdMableTakerProps, |
||||
|
) { |
||||
|
const tableData = React.useMemo( |
||||
|
() => data, |
||||
|
[data], |
||||
|
); |
||||
|
|
||||
|
const toast = useToast(); |
||||
|
|
||||
|
let { run: acceptOrder, isLoading: isAccepting } = useAsync({ |
||||
|
deferFn: async ([orderId]: any[]) => { |
||||
|
try { |
||||
|
let payload = { |
||||
|
order_id: orderId, |
||||
|
}; |
||||
|
await postAcceptOrder(payload); |
||||
|
} catch (e) { |
||||
|
const description = typeof e === "string" ? e : JSON.stringify(e); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Error", |
||||
|
description, |
||||
|
status: "error", |
||||
|
duration: 9000, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
let { run: rejectOrder, isLoading: isRejecting } = useAsync({ |
||||
|
deferFn: async ([orderId]: any[]) => { |
||||
|
try { |
||||
|
let payload = { |
||||
|
order_id: orderId, |
||||
|
}; |
||||
|
await postRejectOrder(payload); |
||||
|
} catch (e) { |
||||
|
const description = typeof e === "string" ? e : JSON.stringify(e); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Error", |
||||
|
description, |
||||
|
status: "error", |
||||
|
duration: 9000, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const columns: Array<Column<Cfd>> = React.useMemo( |
||||
|
() => [ |
||||
|
{ |
||||
|
id: "expander", |
||||
|
Header: () => null, |
||||
|
Cell: ({ row }: any) => ( |
||||
|
<span {...row.getToggleRowExpandedProps()}> |
||||
|
{row.isExpanded |
||||
|
? <IconButton |
||||
|
aria-label="Reduce" |
||||
|
icon={<ChevronUpIcon />} |
||||
|
onClick={() => { |
||||
|
row.toggleRowExpanded(); |
||||
|
}} |
||||
|
/> |
||||
|
: <IconButton |
||||
|
aria-label="Expand" |
||||
|
icon={<ChevronRightIcon />} |
||||
|
onClick={() => { |
||||
|
row.toggleRowExpanded(); |
||||
|
}} |
||||
|
/>} |
||||
|
</span> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
Header: "OrderId", |
||||
|
accessor: "order_id", |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Position", |
||||
|
accessor: ({ position }) => { |
||||
|
let colorScheme = "green"; |
||||
|
if (position.toLocaleLowerCase() === "buy") { |
||||
|
colorScheme = "purple"; |
||||
|
} |
||||
|
return ( |
||||
|
<Badge colorScheme={colorScheme}>{position}</Badge> |
||||
|
); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
Header: "Quantity", |
||||
|
accessor: ({ quantity_usd }) => { |
||||
|
return (<Dollars amount={quantity_usd} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Leverage", |
||||
|
accessor: "leverage", |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Margin", |
||||
|
accessor: "margin", |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Initial Price", |
||||
|
accessor: ({ initial_price }) => { |
||||
|
return (<Dollars amount={initial_price} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Liquidation Price", |
||||
|
isNumeric: true, |
||||
|
accessor: ({ liquidation_price }) => { |
||||
|
return (<Dollars amount={liquidation_price} />); |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Unrealized P/L", |
||||
|
accessor: ({ profit_usd }) => { |
||||
|
return (<Dollars amount={profit_usd} />); |
||||
|
}, |
||||
|
isNumeric: true, |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Timestamp", |
||||
|
accessor: "state_transition_timestamp", |
||||
|
}, |
||||
|
{ |
||||
|
Header: "Action", |
||||
|
accessor: ({ state, order_id }) => { |
||||
|
if (state.toLowerCase() === "requested") { |
||||
|
return (<HStack> |
||||
|
<IconButton |
||||
|
colorScheme="green" |
||||
|
aria-label="Accept" |
||||
|
icon={<CheckIcon />} |
||||
|
onClick={async () => acceptOrder(order_id)} |
||||
|
isLoading={isAccepting} |
||||
|
/> |
||||
|
<IconButton |
||||
|
colorScheme="red" |
||||
|
aria-label="Reject" |
||||
|
icon={<CloseIcon />} |
||||
|
onClick={async () => rejectOrder(order_id)} |
||||
|
isLoading={isRejecting} |
||||
|
/> |
||||
|
</HStack>); |
||||
|
} |
||||
|
|
||||
|
let colorScheme = "gray"; |
||||
|
if (state.toLowerCase() === "rejected") { |
||||
|
colorScheme = "red"; |
||||
|
} |
||||
|
if (state.toLowerCase() === "contract setup") { |
||||
|
colorScheme = "green"; |
||||
|
} |
||||
|
return ( |
||||
|
<Badge colorScheme={colorScheme}>{state}</Badge> |
||||
|
); |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
[], |
||||
|
); |
||||
|
|
||||
|
// if we mark certain columns only as hidden, they are still around and we can render them in the sub-row
|
||||
|
const hiddenColumns = ["order_id", "leverage", "Unrealized P/L", "state_transition_timestamp"]; |
||||
|
|
||||
|
return ( |
||||
|
<Table |
||||
|
tableData={tableData} |
||||
|
columns={columns} |
||||
|
hiddenColumns={hiddenColumns} |
||||
|
renderDetails={renderRowSubComponent} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function renderRowSubComponent(row: Row<Cfd>) { |
||||
|
// TODO: I would show additional information here such as txids, timestamps, actions
|
||||
|
let cells = row.allCells |
||||
|
.filter((cell) => { |
||||
|
return ["state_transition_timestamp"].includes(cell.column.id); |
||||
|
}) |
||||
|
.map((cell) => { |
||||
|
return cell; |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
Showing some more information here... |
||||
|
<HStack> |
||||
|
{cells.map(cell => ( |
||||
|
<Box key={cell.column.id}> |
||||
|
{cell.column.id} = {cell.render("Cell")} |
||||
|
</Box> |
||||
|
))} |
||||
|
</HStack> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface DollarsProps { |
||||
|
amount: number; |
||||
|
} |
||||
|
function Dollars({ amount }: DollarsProps) { |
||||
|
const price = Math.floor(amount * 100.0) / 100.0; |
||||
|
return ( |
||||
|
<> |
||||
|
$ {price} |
||||
|
</> |
||||
|
); |
||||
|
} |
Loading…
Reference in new issue