feat: market offers

---

Reviewed-by: @schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-06-13 20:57:27 +02:00 committed by GitHub
parent 07a984bb52
commit 5ed0efd67e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1191 additions and 39 deletions

View file

@ -30,6 +30,7 @@ import {
PaymentMethods,
} from "@pages/Account";
import { MyWallet } from "@pages/MyWallet";
import { MarketsOffersPage } from "@pages/Markets";
export function AppRoutes() {
return (
@ -38,6 +39,7 @@ export function AppRoutes() {
<Route path={ROUTES.Login} element={<Login />} />
<Route path={ROUTES.Welcome} element={<Welcome />} />
<Route path={ROUTES.CreateAccount} element={<CreateAccount />} />
<Route path={ROUTES.Markets} element={<MarketsOffersPage />} />
<Route
path={ROUTES.MyWallet}
element={

View file

@ -38,4 +38,5 @@ export const Default = Template.bind({});
Default.args = {
currencyCode: "EUR",
value: 400000.12,
format: "symbol",
};

View file

@ -15,15 +15,19 @@
// =============================================================================
import { useMemo } from "react";
import type { FormatNumberOptions } from "react-intl";
import { useIntl } from "react-intl";
interface CurrencyProps {
type CurrencyFormatType = "symbol" | "code" | "name" | "narrowSymbol";
interface CurrencyProps extends FormatNumberOptions {
currencyCode?: string;
format?: CurrencyFormatType;
value: number;
}
export function Currency(props: CurrencyProps) {
const { currencyCode, value } = props;
const { currencyCode, format, value, ...formatNumberProps } = props;
const intl = useIntl();
const formattedNumber = useMemo(
@ -32,7 +36,7 @@ export function Currency(props: CurrencyProps) {
...(currencyCode
? {
currency: currencyCode,
currencyDisplay: "code",
currencyDisplay: format || "code",
style: "currency",
}
: {
@ -40,6 +44,7 @@ export function Currency(props: CurrencyProps) {
minimumFractionDigits: 2,
maximumFractionDigits: 12,
}),
...formatNumberProps,
}),
[currencyCode, value]
);

View file

@ -0,0 +1,39 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
export const useStyles = createStyles((theme) => ({
primary: {
"thead tr": {
backgroundColor: theme.colors.gray[0],
th: {
borderBottomColor: theme.colors.gray[2],
color: theme.colors.gray[5],
fontSize: 10,
fontWeight: 700,
letterSpacing: "0.05em",
textTransform: "uppercase",
},
},
"tbody tr": {
td: {
borderBottomColor: theme.colors.gray[2],
},
},
},
}));

View file

@ -20,23 +20,32 @@ import {
getExpandedRowModel,
} from "@tanstack/react-table";
import { Table as MTable } from "@mantine/core";
import type { TableProps } from "./_types";
import { TableVariant } from "./_types";
import { TableProvider } from "./use-table-context";
import { TableHeader } from "./TableHeader";
import { TableBody } from "./TableBody";
import type { TableProps } from "./_types";
import { useStyles } from "./Table.style";
export function Table(props: TableProps) {
const { table, columns, data, tableWrap } = props;
const { classes, cx } = useStyles();
const { table, columns, data, tableWrap, variant, state } = props;
const tableInstance = useTableInstance(table, {
data,
columns,
state,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
});
return (
<MTable {...tableWrap}>
<MTable
{...tableWrap}
className={cx(tableWrap?.className, {
[classes.primary]: variant === TableVariant.Primary,
})}
>
<TableProvider value={{ table: tableInstance, props }}>
<TableHeader />
<TableBody />

View file

@ -38,6 +38,7 @@ export function TableBody() {
key={cell.id}
style={{
width: cell.column.getSize(),
textAlign: cell.column.columnDef?.meta?.textAlign,
}}
>
{cell.renderCell()}

View file

@ -36,6 +36,7 @@ export function TableHeader() {
colSpan={header.colSpan}
style={{
width: header.getSize(),
textAlign: header.column.columnDef?.meta?.textAlign,
}}
>
{header.isPlaceholder ? null : header.renderHeader()}

View file

@ -15,13 +15,14 @@
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ColumnDef, Row } from "@tanstack/react-table";
import type { ColumnDef, Row, TableState } from "@tanstack/react-table";
import type { TableProps as MTableProps } from "@mantine/core";
export interface TableProps {
columns: Array<ColumnDef<any>>;
table: any;
data: Array<any>;
state?: Partial<TableState>;
showHeader?: boolean;
showFooter?: boolean;
@ -29,4 +30,10 @@ export interface TableProps {
rowSubComponent?: ({ row }: { row: Row<any> }) => React.ReactNode;
tableWrap?: MTableProps;
variant?: TableVariant;
}
export enum TableVariant {
Default = "Default",
Primary = "Primary",
}

View file

@ -87,4 +87,4 @@ const data = [
height: 2482937,
fee: 0.000005096,
},
] as TWalletTransaction[];
] as Array<TWalletTransaction>;

View file

@ -0,0 +1,58 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MarketOffersTable } from "./MarketOffersTable";
export default {
title: "molecules/MarketOffersTable",
component: MarketOffersTable,
} as ComponentMeta<typeof MarketOffersTable>;
const Template: ComponentStory<typeof MarketOffersTable> = () => {
return <MarketOffersTable data={data} />;
};
export const Default = Template.bind({});
Default.args = {};
const data = [
{
price: 123,
priceCurrency: "EUR",
priceComparison: 0.12,
amount: 123123,
amountCurrency: "EUR",
cost: 123,
costCurrency: "USD",
paymentMethod: "Bitcoin",
accountAge: 12,
accountTrades: 1212,
},
{
price: 123,
priceCurrency: "EUR",
priceComparison: 0.12,
amount: 123123,
amountCurrency: "EUR",
cost: 123,
costCurrency: "USD",
paymentMethod: "Altcoins",
accountAge: 12,
accountTrades: 1212,
},
];

View file

@ -0,0 +1,116 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { MarketOffersTable } from "./MarketOffersTable";
import { AppProviders } from "@atoms/AppProviders";
describe("molecules::MarketOffersTable", () => {
it("renders without exploding.", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all columns.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Price")).toBeInTheDocument();
expect(screen.queryByText("Amount")).toBeInTheDocument();
expect(screen.queryByText("Costs")).toBeInTheDocument();
expect(screen.queryByText("Payment Method")).toBeInTheDocument();
unmount();
});
it("renders formatted price value with currency sign.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("€5,000.96")).toBeInTheDocument();
expect(screen.queryByText("€9,637.41")).toBeInTheDocument();
unmount();
});
it("renders formatted amount value with currency code.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("XMR 7,564.94")).toBeInTheDocument();
expect(screen.queryByText("XMR 6,483.23")).toBeInTheDocument();
unmount();
});
it("renders offer formatted payment method.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Bitcoin")).toBeInTheDocument();
expect(screen.queryByText("Altcoins")).toBeInTheDocument();
unmount();
});
it("renders formatted price percentage.", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTable data={data} />
</AppProviders>
);
expect(screen.queryByText("12%")).toBeInTheDocument();
expect(screen.queryByText("15%")).toBeInTheDocument();
unmount();
});
});
const data = [
{
price: 5000.956,
priceComparison: 0.12,
priceCurrency: "EUR",
amount: 7564.94,
amountCurrency: "XMR",
cost: 532.34,
costCurrency: "USD",
paymentMethod: "Bitcoin",
accountAge: 12,
accountTrades: 1212,
},
{
price: 9637.41,
priceComparison: 0.15,
priceCurrency: "EUR",
amount: 6483.23,
amountCurrency: "XMR",
cost: 983.32,
costCurrency: "USD",
paymentMethod: "Altcoins",
accountAge: 12,
accountTrades: 3412,
},
];

View file

@ -0,0 +1,150 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
import { createTable } from "@tanstack/react-table";
import { useIntl } from "react-intl";
import type { MarketOffer } from "./_types";
import {
MarketOffersPriceCell,
MarketOffersAmountCell,
MarketOffersAccountTradesCell,
MarketOffersCostsCell,
MarketOffersAccountAgeCell,
MarketOffersPaymentCell,
} from "./MarketOffersTableCell";
import { Table } from "@molecules/Table";
import { LangKeys } from "@constants/lang";
import { TableVariant } from "@molecules/Table/_types";
const table = createTable().setRowType<MarketOffer>();
interface MarketOffersTableProps {
data: Array<MarketOffer>;
}
export function MarketOffersTable({ data }: MarketOffersTableProps) {
const { classes } = useStyles();
const columns = useMarketOffersColumns();
return (
<Table
table={table}
columns={columns}
data={data}
variant={TableVariant.Primary}
state={{
columnVisibility: {
accountAge: false,
accountTrades: false,
},
}}
tableWrap={{
verticalSpacing: "md",
className: classes.root,
}}
/>
);
}
const useStyles = createStyles((theme) => ({
root: {
"thead tr th, tbody tr td": {
"&:first-of-type": {
paddingLeft: 30,
},
"&:last-of-type": {
paddingRight: 30,
},
},
"tbody tr": {
td: {
background: theme.white,
paddingTop: 22,
paddingBottom: 22,
},
"&:last-of-type": {
td: {
borderBottom: `1px solid ${theme.colors.gray[2]}`,
},
},
},
},
}));
const useMarketOffersColumns = () => {
const { formatMessage } = useIntl();
return [
table.createDataColumn("price", {
id: "price",
header: formatMessage({
id: LangKeys.MarketsOffersColumnPrice,
defaultMessage: "Price",
}),
cell: ({ row }) => <MarketOffersPriceCell row={row.original} />,
size: 400,
}),
table.createDataColumn("amount", {
id: "amount",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAmount,
defaultMessage: "Amount",
}),
cell: ({ row }) => <MarketOffersAmountCell row={row.original} />,
size: 400,
}),
table.createDataColumn("cost", {
id: "costs",
header: formatMessage({
id: LangKeys.MarketsOffersColumnCost,
defaultMessage: "Costs",
}),
cell: ({ row }) => <MarketOffersCostsCell row={row.original} />,
size: 400,
}),
table.createDataColumn("paymentMethod", {
id: "paymentMethod",
header: formatMessage({
id: LangKeys.MarketsOffersColumnPaymentMethod,
defaultMessage: "Payment Method",
}),
cell: ({ row }) => <MarketOffersPaymentCell row={row.original} />,
size: 400,
}),
table.createDataColumn("accountAge", {
id: "accountAge",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAccountAge,
defaultMessage: "Account Age",
}),
cell: ({ row }) => <MarketOffersAccountAgeCell row={row.original} />,
size: 400,
}),
table.createDataColumn("accountTrades", {
id: "accountTrades",
header: formatMessage({
id: LangKeys.MarketsOffersColumnAccountTrades,
defaultMessage: "Account Trades",
}),
cell: ({ row }) => <MarketOffersAccountTradesCell row={row.original} />,
size: 400,
meta: {
textAlign: "right",
},
}),
];
};

View file

@ -0,0 +1,87 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Group, useMantineTheme } from "@mantine/core";
import type { MarketOffer } from "./_types";
import { Currency } from "@atoms/Currency";
import { BodyText } from "@atoms/Typography";
import { fractionToPercent } from "@src/utils/math";
import { ReactComponent as CheckCircle } from "@assets/check-circle.svg";
export function MarketOffersAccountAgeCell({ row }: { row?: MarketOffer }) {
const theme = useMantineTheme();
return (
<Group spacing="sm">
<CheckCircle color={theme.colors.blue[6]} width={15} height={15} />
<BodyText heavy>{row?.accountAge} Days</BodyText>
</Group>
);
}
export function MarketOffersPriceCell({ row }: { row?: MarketOffer }) {
return (
<Group spacing="sm">
<BodyText heavy>
<Currency
currencyCode={row?.priceCurrency}
value={row?.price || 0}
format="symbol"
/>
</BodyText>
<BodyText color="gray">
<Currency
value={fractionToPercent(row?.priceComparison || 0)}
minimumFractionDigits={0}
/>
%
</BodyText>
</Group>
);
}
export function MarketOffersAmountCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency currencyCode={row?.amountCurrency} value={row?.amount || 0} />
</BodyText>
);
}
export function MarketOffersCostsCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency
currencyCode={row?.costCurrency}
value={row?.cost || 0}
format="symbol"
/>
</BodyText>
);
}
export function MarketOffersAccountTradesCell({ row }: { row?: MarketOffer }) {
return (
<BodyText heavy>
<Currency value={row?.accountTrades || 0} minimumFractionDigits={0} />
</BodyText>
);
}
export function MarketOffersPaymentCell({ row }: { row?: MarketOffer }) {
return <BodyText heavy>{row?.paymentMethod}</BodyText>;
}

View file

@ -0,0 +1,134 @@
// Vitest Snapshot v1
exports[`molecules::MarketOffersTable > renders without exploding. 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root mantine-1ei5v84"
>
<thead>
<tr>
<th
colspan="1"
style="width: 400px;"
>
Price
</th>
<th
colspan="1"
style="width: 400px;"
>
Amount
</th>
<th
colspan="1"
style="width: 400px;"
>
Costs
</th>
<th
colspan="1"
style="width: 400px;"
>
Payment Method
</th>
</tr>
</thead>
<tbody>
<tr>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-1lumg83"
>
<div
class="mantine-Text-root mantine-Group-child mantine-1h17kkk"
>
€5,000.96
</div>
<div
class="mantine-Text-root mantine-Group-child mantine-1qscdi2"
>
12%
</div>
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
XMR 7,564.94
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
$532.34
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
Bitcoin
</div>
</td>
</tr>
<tr>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-1lumg83"
>
<div
class="mantine-Text-root mantine-Group-child mantine-1h17kkk"
>
€9,637.41
</div>
<div
class="mantine-Text-root mantine-Group-child mantine-1qscdi2"
>
15%
</div>
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
XMR 6,483.23
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
$983.32
</div>
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-ix3vgq"
>
Altcoins
</div>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View file

@ -0,0 +1,28 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export interface MarketOffer {
price: number;
priceComparison: number;
priceCurrency: string;
amount: number;
amountCurrency: string;
cost: number;
costCurrency: string;
paymentMethod: string;
accountAge: number;
accountTrades: number;
}

View file

@ -0,0 +1,18 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export * from "./MarketOffersTable";
export * from "./_types";

View file

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { MarketsOffers } from "./MarketsOffers";
export default {
title: "organisms/MarketsOffers",
component: MarketsOffers,
} as ComponentMeta<typeof MarketsOffers>;
const Template: ComponentStory<typeof MarketsOffers> = () => {
return <MarketsOffers />;
};
export const Default = Template.bind({});
Default.args = {};

View file

@ -0,0 +1,57 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { FC } from "react";
import { useMemo } from "react";
import { Group, Loader } from "@mantine/core";
import { transformToMarketsOffers } from "./_utils";
import { useMarketsOffers } from "@hooks/haveno/useMarketsOffers";
import { MarketOffersTable } from "@organisms/MarketOffersTable";
export function MarketsOffersLoaded() {
const { data } = useMarketsOffers({
assetCode: "ETH",
direction: "sell",
});
const tableData = useMemo(() => transformToMarketsOffers(data || []), [data]);
if (!data) {
return null;
}
return <MarketOffersTable data={tableData} />;
}
const MarketsOffersBoot: FC = ({ children }) => {
const { isLoading: isOffersLoading } = useMarketsOffers({
assetCode: "ETH",
direction: "buy",
});
return isOffersLoading ? (
<Group>
<Loader color="gray" style={{ margin: "auto" }} />
</Group>
) : (
<>{children}</>
);
};
export function MarketsOffers() {
return (
<MarketsOffersBoot>
<MarketsOffersLoaded />
</MarketsOffersBoot>
);
}

View file

@ -0,0 +1,35 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { MarketOfferData } from "@hooks/haveno/useMarketsOffers";
import type { MarketOffer } from "@organisms/MarketOffersTable";
export const transformToMarketsOffers = (
offers: Array<MarketOfferData>
): Array<MarketOffer> => {
return offers.map((offer) => ({
price: offer.price,
priceCurrency: offer.counterCurrencyCode,
amount: offer.amount,
amountCurrency: offer.baseCurrencyCode,
costCurrency: offer.baseCurrencyCode,
paymentMethod: offer.paymentMethodShortName,
cost: offer.txFee,
priceComparison: 0.1,
accountAge: 1,
accountTrades: 1,
}));
};

View file

@ -0,0 +1,17 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export * from "./MarketsOffers";

View file

@ -16,17 +16,20 @@
import { UnstyledButton, Group, Text, createStyles } from "@mantine/core";
import type { ReactNode } from "react";
import { Link } from "react-router-dom";
interface NavLinkProps {
icon: ReactNode;
isActive?: boolean;
label: string;
link: string;
}
export function NavLink({ icon, isActive = false, label }: NavLinkProps) {
export function NavLink({ icon, isActive = false, label, link }: NavLinkProps) {
const { classes } = useStyles({ isActive });
return (
<UnstyledButton className={classes.navLink}>
<UnstyledButton component={Link} className={classes.navLink} to={link}>
<Group>
{icon}
<Text

View file

@ -32,9 +32,9 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/markets"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -58,14 +58,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Markets
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -93,14 +93,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
My Offers
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -132,14 +132,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
My Trades
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -163,14 +163,14 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Notifications
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"
>
<button
<a
class="mantine-UnstyledButton-root mantine-1fq8ags"
type="button"
href="#/"
>
<div
class="mantine-Group-root mantine-6y1794"
@ -193,7 +193,7 @@ exports[`molecules::Sidebar > renders without exploding 1`] = `
Account
</div>
</div>
</button>
</a>
</div>
<div
class="mantine-khtkeg"

View file

@ -14,6 +14,7 @@
// limitations under the License.
// =============================================================================
import type { ReactNode } from "react";
import { ReactComponent as MarketsIcon } from "@assets/markets.svg";
import { ReactComponent as OffersIcon } from "@assets/offers.svg";
import { ReactComponent as TradesIcon } from "@assets/trades.svg";
@ -22,25 +23,36 @@ import { ReactComponent as AccountIcon } from "@assets/account.svg";
export const WIDTH = 210;
interface NavigationLink {
icon: ReactNode;
label: string;
link: string;
}
export const NAV_LINKS = [
{
icon: <MarketsIcon />,
label: "Markets",
link: "/markets",
},
{
icon: <OffersIcon />,
label: "My Offers",
link: "/",
},
{
icon: <TradesIcon />,
label: "My Trades",
link: "/",
},
{
icon: <NotificationsIcon />,
label: "Notifications",
link: "/",
},
{
icon: <AccountIcon />,
label: "Account",
link: "/",
},
];
] as Array<NavigationLink>;

View file

@ -19,7 +19,7 @@ import { NavbarLayout } from "@templates/NavbarLayout";
import { AccountSidebar } from "@organisms/AccountSidebar";
interface AccountContentProps {
children: JSX.Element | JSX.Element[];
children: JSX.Element | Array<JSX.Element>;
}
function AccountContent({ children }: AccountContentProps) {
@ -34,7 +34,7 @@ function AccountContent({ children }: AccountContentProps) {
}
interface AccountLayoutProps {
children: JSX.Element | JSX.Element[];
children: JSX.Element | Array<JSX.Element>;
}
export function AccountLayout({ children }: AccountLayoutProps) {

View file

@ -14,15 +14,24 @@
// limitations under the License.
// =============================================================================
import { Box, createStyles, Group } from "@mantine/core";
import type { FC } from "react";
import type { DefaultProps } from "@mantine/core";
import { Box, createStyles, Group } from "@mantine/core";
import { Sidebar } from "@organisms/Sidebar";
export const NavbarLayout: FC = (props) => {
const { children } = props;
const { classes } = useStyles();
export const NavbarLayout: FC<DefaultProps> = ({
children,
classNames,
className,
...rest
}) => {
const { classes, cx } = useStyles(undefined, {
name: "NavbarLayout",
classNames,
});
return (
<Group className={classes.container} spacing={0}>
<Group className={cx(classes.container, className)} spacing={0} {...rest}>
<Sidebar />
<Box className={classes.contentArea}>{children}</Box>
</Group>

View file

@ -99,4 +99,11 @@ export enum LangKeys {
AccountBackupRestoreBtn = "account.backup.restore.btn",
AccountBackupDownloadSuccessNotif = "account.backup.download.successNotification",
AccountBackupRestoreSuccessNotif = "account.backup.restore.successNotification",
MarketsOffersColumnPrice = "marketsOffers.columnPrice",
MarketsOffersColumnAmount = "marketsOffers.columnAmount",
MarketsOffersColumnCost = "marketsOffers.columnCost",
MarketsOffersColumnPaymentMethod = "marketsOffers.columnPaymentMethod",
MarketsOffersColumnAccountAge = "marketsOffers.columnAccountAge",
MarketsOffersColumnAccountTrades = "marketsOffers.columnAccountTypes",
}

View file

@ -116,6 +116,12 @@ const LangPackEN: { [key in LangKeys]: string } = {
"The backup has been downloaded successfully.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"The backup has been restored successfully.",
[LangKeys.MarketsOffersColumnPrice]: "Price",
[LangKeys.MarketsOffersColumnAmount]: "Amount",
[LangKeys.MarketsOffersColumnCost]: "Costs",
[LangKeys.MarketsOffersColumnAccountAge]: "Account Age",
[LangKeys.MarketsOffersColumnAccountTrades]: "Account Trades",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Payment Method",
};
export default LangPackEN;

View file

@ -119,6 +119,12 @@ const LangPackES: { [key in LangKeys]: string } = {
"La copia de seguridad se ha descargado correctamente.",
[LangKeys.AccountBackupRestoreSuccessNotif]:
"La copia de seguridad se ha restaurado correctamente.",
[LangKeys.MarketsOffersColumnPrice]: "Precio",
[LangKeys.MarketsOffersColumnAmount]: "Monto",
[LangKeys.MarketsOffersColumnCost]: "Costos",
[LangKeys.MarketsOffersColumnAccountAge]: "Edad de la cuenta",
[LangKeys.MarketsOffersColumnAccountTrades]: "Operaciones de cuenta",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Método de pago",
};
export default LangPackES;

View file

@ -29,6 +29,8 @@ export enum QueryKeys {
XmrSeed = "Haveno.XmrSeed",
XmrPrimaryAddress = "Haveno.XmrPrimaryAddress",
XmrTxs = "Haveno.XmrTransactions",
MarketsOffers = "Haveno.MarketsOffers",
MyOffers = "Haveno.MyOffers",
// Storage
StorageAccountInfo = "Storage.AccountInfo",

View file

@ -22,6 +22,7 @@ export const ROUTES = {
CreateAccount: "/onboarding/create-account",
RestoreBackup: "/onboarding/restore-backup",
MyWallet: "/my-wallet",
Markets: "/markets",
// Account routes
PaymentAccounts: "/account/payment-accounts",

View file

@ -0,0 +1,82 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useQuery } from "react-query";
import type { OfferInfo } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface MarketsOfferesQuery {
assetCode: string;
direction?: "buy" | "sell";
}
export function useMarketsOffers(query: MarketsOfferesQuery) {
const client = useHavenoClient();
return useQuery<Array<MarketOfferData>, Error>(
[QueryKeys.MarketsOffers, query],
async () => {
const offers = await client.getMyOffers(query.assetCode);
return transformData(offers);
}
);
}
const transformData = (offers: Array<OfferInfo>) => {
return offers.map((offerObj: OfferInfo): MarketOfferData => {
const offer = offerObj.toObject();
return {
...offer,
price: parseFloat(offer.price),
volume: parseFloat(offer.volume),
minVolume: parseFloat(offer.minVolume),
triggerPrice: parseFloat(offer.triggerPrice),
};
});
};
export interface MarketOfferData {
id: string;
direction: string;
price: number;
useMarketBasedPrice: boolean;
marketPriceMarginPct: number;
amount: number;
minAmount: number;
volume: number;
minVolume: number;
buyerSecurityDeposit: number;
triggerPrice: number;
paymentAccountId: string;
paymentMethodId: string;
paymentMethodShortName: string;
baseCurrencyCode: string;
counterCurrencyCode: string;
date: number;
state: string;
sellerSecurityDeposit: number;
offerFeePaymentTxId: string;
txFee: number;
makerFee: number;
isActivated: boolean;
isMyOffer: boolean;
ownerNodeAddress: string;
pubKeyRing: string;
versionNr: string;
protocolVersion: number;
}

View file

@ -0,0 +1,33 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useQuery } from "react-query";
import type { OfferInfo } from "haveno-ts";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface MyOfferesQuery {
assetCode: string;
direction?: "buy" | "sell";
}
export function useMarketsOffers(query: MyOfferesQuery) {
const client = useHavenoClient();
return useQuery<Array<OfferInfo>, Error>([QueryKeys.MyOffers, query], () =>
client.getMyOffers(query.assetCode, query.direction)
);
}

View file

@ -0,0 +1,54 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation, useQueryClient } from "react-query";
import { showNotification } from "@mantine/notifications";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface SetCryptoPaymentAccount {
accountName: string;
assetCode: string;
address: string;
}
export function useSetCryptoPaymentAccount() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation(
async (variables: SetCryptoPaymentAccount) => {
return client.createCryptoPaymentAccount(
variables.accountName,
variables.assetCode,
variables.address
);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.PaymentAccounts);
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message || "",
title: "Something went wrong",
});
},
}
);
}

View file

@ -0,0 +1,67 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { useMutation, useQueryClient } from "react-query";
import { showNotification } from "@mantine/notifications";
import { useHavenoClient } from "./useHavenoClient";
import { QueryKeys } from "@constants/query-keys";
interface SetOfferVariables {
direction: string;
amount: bigint;
assetCode: string;
paymentAccountId: string;
buyerSecurityDeposit: number;
price?: number;
marketPriceMargin?: number;
triggerPrice?: number;
minAmount?: bigint;
}
export function useSetOffer() {
const client = useHavenoClient();
const queryClient = useQueryClient();
return useMutation(
async (variables: SetOfferVariables) => {
return client.postOffer(
variables.direction,
variables.amount,
variables.assetCode,
variables.paymentAccountId,
variables.buyerSecurityDeposit,
variables.price,
variables.marketPriceMargin,
variables.triggerPrice,
variables.minAmount
);
},
{
onSuccess: () => {
queryClient.invalidateQueries(QueryKeys.MarketsOffers);
queryClient.invalidateQueries(QueryKeys.MyOffers);
},
onError: (err: Error) => {
console.dir(err);
showNotification({
color: "red",
message: err.message || "",
title: "Something went wrong",
});
},
}
);
}

View file

@ -0,0 +1,35 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
import { NavbarLayout } from "@templates/NavbarLayout";
import { MarketsOffers } from "@organisms/MarketsOffers";
export function MarketsOffersPage() {
const { classes } = useStyles();
return (
<NavbarLayout classNames={{ contentArea: classes.contentArea }}>
<MarketsOffers />
</NavbarLayout>
);
}
const useStyles = createStyles(() => ({
contentArea: {
padding: 0,
},
}));

View file

@ -0,0 +1,17 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export * from "./MarketsOffers";

View file

@ -0,0 +1,19 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
export const fractionToPercent = (value: number) => {
return value * 100;
};