feat: market filters

---

Co-authored-by: @schowdhuri 
Reviewed-by: @schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-06-15 00:17:01 +02:00 committed by GitHub
parent 1c0e371c0a
commit fcd209fd3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 3515 additions and 9 deletions

View File

@ -39,7 +39,14 @@ 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.Markets}
element={
<ProtectedRoute>
<MarketsOffersPage />
</ProtectedRoute>
}
/>
<Route
path={ROUTES.MyWallet}
element={

View File

@ -0,0 +1,51 @@
// =============================================================================
// 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 { ReactNode } from "react";
import type { BoxProps } from "@mantine/core";
import { Box, createStyles } from "@mantine/core";
interface AmountChangeProps extends BoxProps<"div"> {
children: ReactNode;
positive?: boolean;
negative?: boolean;
}
export function AmountChange(props: AmountChangeProps) {
const { children, positive = false, negative = false } = props;
const { classes } = useStyles({
positive,
negative,
});
return <Box className={classes.root}>{children}</Box>;
}
interface AmountChangeStyleProps {
positive: boolean;
negative: boolean;
}
const useStyles = createStyles(
(theme, { positive, negative }: AmountChangeStyleProps) => ({
root: {
color: negative
? theme.colors.red[6]
: positive
? theme.colors.green[6]
: undefined,
},
})
);

View File

@ -14,8 +14,15 @@
// limitations under the License.
// =============================================================================
import { createStyles, TextInput as MTextInput } from "@mantine/core";
import type { TextInputProps as MTextInputProps } from "@mantine/core";
import type {
TextInputProps as MTextInputProps,
NumberInputProps as MNumberInputProps,
} from "@mantine/core";
import {
createStyles,
TextInput as MTextInput,
NumberInput as MNumberInput,
} from "@mantine/core";
interface TextInputProps extends MTextInputProps {
id: string;
@ -24,9 +31,21 @@ interface TextInputProps extends MTextInputProps {
export function TextInput(props: TextInputProps) {
const { id, ...rest } = props;
const { classes } = useStyles();
return <MTextInput classNames={classes} id={id} {...rest} />;
}
interface NumberInputProps extends MNumberInputProps {
id: string;
}
export function NumberInput(props: NumberInputProps) {
const { id, ...rest } = props;
const { classes } = useStyles();
return <MNumberInput classNames={classes} id={id} {...rest} />;
}
const useStyles = createStyles((theme) => ({
label: {
fontSize: "0.875rem",

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 type { ComponentStory, ComponentMeta } from "@storybook/react";
import { ToggleButton } from "./ToggleButton";
export default {
title: "atoms/ToggleButton",
component: ToggleButton,
} as ComponentMeta<typeof ToggleButton>;
const Template: ComponentStory<typeof ToggleButton> = (args) => {
return <ToggleButton {...args} />;
};
export const Default = Template.bind({});
Default.args = {
labels: ["Sell XMR", "Buy XMR"],
};

View File

@ -0,0 +1,38 @@
// =============================================================================
// 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 { ToggleButton } from "./ToggleButton";
describe("atoms::ToggleButton", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<ToggleButton labels={["Sell", "Buy"]} />
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all tabs", () => {
const { unmount } = render(
<ToggleButton labels={["Sell XMR", "Buy XMR"]} />
);
expect(screen.queryByText("Sell XMR")).toBeInTheDocument();
expect(screen.queryByText("Buy XMR")).toBeInTheDocument();
unmount();
});
});

View File

@ -0,0 +1,97 @@
// =============================================================================
// 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, Tabs } from "@mantine/core";
interface ToggleButtonProps {
labels: Array<string>;
onChange?: (selectedIndex: number) => void;
active?: number;
}
export function ToggleButton({ labels, onChange, active }: ToggleButtonProps) {
const { classes } = useStyles();
const handleChange = (tabIndex: number) => {
onChange && onChange(tabIndex);
};
return (
<Tabs
variant="unstyled"
classNames={classes}
active={active}
onTabChange={handleChange}
>
{labels.map((label, index) => (
<Tabs.Tab key={index} label={label} />
))}
</Tabs>
);
}
const useStyles = createStyles((theme) => ({
tabControl: {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[0],
border: `0 solid ${
theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[2]
}`,
borderBottomWidth: 1,
borderTopWidth: 1,
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[9],
fontSize: theme.fontSizes.md,
fontWeight: 500,
padding: `${theme.spacing.lg}px ${theme.spacing.md}px`,
"&:not(:first-of-type)": {
borderLeft: 0,
},
"&:first-of-type": {
borderTopLeftRadius: theme.radius.md,
borderBottomLeftRadius: theme.radius.md,
borderLeftWidth: 1,
},
"&:last-of-type": {
borderTopRightRadius: theme.radius.md,
borderBottomRightRadius: theme.radius.md,
borderRightWidth: 1,
},
},
tabActive: {
color: theme.white,
position: "relative",
"&:before": {
backgroundColor: theme.colors.blue[6],
borderRadius: theme.radius.md,
bottom: -1,
content: `""`,
left: -1,
position: "absolute",
right: -1,
top: -1,
},
},
tabInner: {
position: "relative",
zIndex: 1,
},
}));

View File

@ -0,0 +1,54 @@
// Vitest Snapshot v1
exports[`atoms::ToggleButton > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Tabs-root mantine-jjhffj"
>
<div
class="mantine-1s8spa1 mantine-Tabs-tabsListWrapper"
>
<div
aria-orientation="horizontal"
class="mantine-Group-root __mantine-ref-tabsList mantine-Tabs-tabsList mantine-1tggmaq"
role="tablist"
>
<button
aria-selected="true"
class="mantine-Tabs-tabControl __mantine-ref-tabActive mantine-Tabs-tabActive mantine-Group-child mantine-9dd9nw"
role="tab"
tabindex="0"
type="button"
>
<div
class="mantine-Tabs-tabInner mantine-12bvju6"
>
<div
class="mantine-1s8spa1 mantine-Tabs-tabLabel"
>
Sell
</div>
</div>
</button>
<button
aria-selected="false"
class="mantine-Tabs-tabControl mantine-Group-child mantine-1pl658a"
role="tab"
tabindex="-1"
type="button"
>
<div
class="mantine-Tabs-tabInner mantine-12bvju6"
>
<div
class="mantine-1s8spa1 mantine-Tabs-tabLabel"
>
Buy
</div>
</div>
</button>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,154 @@
// =============================================================================
// 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 { useState, useEffect } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { createTable } from "@tanstack/react-table";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Table } from "./Table";
export default {
title: "atoms/Table/EditableTable",
component: Table,
} as ComponentMeta<typeof Table>;
const Template: ComponentStory<typeof Table> = () => {
return (
<Table
table={table}
data={data}
columns={columns}
defaultColumn={defaultColumn}
onEditableDataChange={(values) => {
console.log(values);
}}
/>
);
};
export const Default = Template.bind({});
Default.args = {};
interface Person {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
}
const table = createTable().setRowType<Person>().setTableMetaType<{
updateData: (rowIndex: number, columnId: string, value: unknown) => void;
}>();
type TableGenerics = typeof table.generics;
const columns = [
table.createGroup({
header: "Name",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("firstName", {
cell: (info) => info.getValue(),
footer: (props) => props.column.id,
}),
table.createDataColumn((row) => row.lastName, {
id: "lastName",
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
footer: (props) => props.column.id,
}),
],
}),
table.createGroup({
header: "Info",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("age", {
header: () => "Age",
footer: (props) => props.column.id,
}),
table.createGroup({
header: "More Info",
columns: [
table.createDataColumn("visits", {
header: () => <span>Visits</span>,
footer: (props) => props.column.id,
}),
table.createDataColumn("status", {
header: "Status",
footer: (props) => props.column.id,
}),
table.createDataColumn("progress", {
header: "Profile Progress",
footer: (props) => props.column.id,
}),
],
}),
],
}),
];
const data: Array<Person> = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
const defaultColumn: Partial<ColumnDef<TableGenerics>> = {
cell: ({ getValue, row: { index }, column: { id }, instance }) => {
const initialValue = getValue();
const [value, setValue] = useState(initialValue);
const onBlur = () => {
instance.options.meta?.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<input
value={value as string}
onChange={(e) => setValue(e.target.value)}
onBlur={onBlur}
/>
);
},
};

View File

@ -26,10 +26,20 @@ import { TableProvider } from "./use-table-context";
import { TableHeader } from "./TableHeader";
import { TableBody } from "./TableBody";
import { useStyles } from "./Table.style";
import { updateTableCell } from "./_utils";
export function Table(props: TableProps) {
const { classes, cx } = useStyles();
const { table, columns, data, tableWrap, variant, state } = props;
const {
table,
columns,
data,
tableWrap,
variant,
onEditableDataChange,
defaultColumn,
state,
} = props;
const tableInstance = useTableInstance(table, {
data,
@ -37,6 +47,17 @@ export function Table(props: TableProps) {
state,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
meta: {
updateData: (rowIndex: number, columnId: string, value: unknown) => {
const newData = updateTableCell(data, rowIndex, columnId, value);
onEditableDataChange && onEditableDataChange(newData);
},
},
...(defaultColumn
? {
defaultColumn,
}
: {}),
});
return (

View File

@ -20,7 +20,7 @@ import { useTableContext } from "./use-table-context";
export function TableBody() {
const {
table,
props: { rowSubComponent },
props: { rowSubComponent, onRowClick },
} = useTableContext();
return (
@ -31,6 +31,7 @@ export function TableBody() {
key={row.id}
onClick={() => {
row.toggleExpanded();
onRowClick && onRowClick(row);
}}
>
{row.getVisibleCells().map((cell) => (

View File

@ -18,11 +18,13 @@
import type { ColumnDef, Row, TableState } from "@tanstack/react-table";
import type { TableProps as MTableProps } from "@mantine/core";
// TODO: Add type or generic
export interface TableProps {
columns: Array<ColumnDef<any>>;
table: any;
data: Array<any>;
state?: Partial<TableState>;
defaultColumn?: any;
showHeader?: boolean;
showFooter?: boolean;
@ -31,6 +33,11 @@ export interface TableProps {
tableWrap?: MTableProps;
variant?: TableVariant;
onEditableDataChange?: (v: Array<any>) => void;
pointerRow?: boolean;
onRowClick?: (column: any) => void;
}
export enum TableVariant {

View File

@ -0,0 +1,34 @@
// =============================================================================
// 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 updateTableCell = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: Array<any>,
rowIndex: number,
columnId: string,
value: unknown
) => {
return data.map((row, index) => {
if (index === rowIndex) {
return {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
...data[rowIndex]!,
[columnId]: value,
};
}
return row;
});
};

View File

@ -0,0 +1,128 @@
// =============================================================================
// 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 { createTable } from "@tanstack/react-table";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Table } from "../Table";
import { CheckboxCell } from "./CheckboxCell";
export default {
title: "atoms/Table/CheckboxCell",
component: Table,
} as ComponentMeta<typeof Table>;
const Template: ComponentStory<typeof Table> = (args) => {
return (
<Table
table={table}
columns={columns}
data={args.data}
onEditableDataChange={(values) => {
console.log(values);
}}
/>
);
};
export const Default = Template.bind({});
interface Person {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
}
const table = createTable().setRowType<Person>().setTableMetaType<{
updateData: (rowIndex: number, columnId: string, value: unknown) => void;
}>();
const columns = [
table.createGroup({
header: "Name",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("firstName", {
cell: (params) => <CheckboxCell {...params} />,
footer: (props) => props.column.id,
}),
table.createDataColumn((row) => row.lastName, {
id: "lastName",
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
footer: (props) => props.column.id,
}),
],
}),
table.createGroup({
header: "Info",
footer: (props) => props.column.id,
columns: [
table.createDataColumn("age", {
header: () => "Age",
footer: (props) => props.column.id,
}),
table.createGroup({
header: "More Info",
columns: [
table.createDataColumn("visits", {
header: () => <span>Visits</span>,
footer: (props) => props.column.id,
}),
table.createDataColumn("status", {
header: "Status",
footer: (props) => props.column.id,
}),
table.createDataColumn("progress", {
header: "Profile Progress",
footer: (props) => props.column.id,
}),
],
}),
],
}),
];
Default.args = {
data: [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
] as Array<Person>,
};

View File

@ -0,0 +1,59 @@
// =============================================================================
// 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.
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CheckboxProps } from "@mantine/core";
import { Checkbox } from "@mantine/core";
import type { Cell, Column, Row, TableInstance } from "@tanstack/react-table";
import { useState, useEffect } from "react";
// TODO: Add type or generic
interface CheckboxCellProps {
instance: TableInstance<any>;
row: Row<any>;
column: Column<any>;
cell: Cell<any>;
getValue: () => any;
checkboxProps?: CheckboxProps;
}
export const CheckboxCell = ({
getValue,
row: { index },
column: { id },
instance,
checkboxProps,
}: CheckboxCellProps) => {
const initialValue = getValue();
const [value, setValue] = useState<boolean>(initialValue);
const onBlur = () => {
instance.options.meta?.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<Checkbox
id={`${id}-${index}`}
checked={value as boolean}
onChange={(e) => setValue(e.target.checked)}
onBlur={onBlur}
{...checkboxProps}
/>
);
};

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 "./CheckboxCell";

View File

@ -14,4 +14,6 @@
// limitations under the License.
// =============================================================================
export * from "./_types";
export * from "./Table";
export * from "./cells";

View File

@ -20,6 +20,7 @@ import type { TableInstance } from "@tanstack/react-table";
import type { TableProps } from "../_types";
interface TableContextValue {
// TODO: Add type or generic
table: TableInstance<any>;
props: TableProps;
}

View File

@ -0,0 +1,30 @@
// =============================================================================
// 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 { MarketOffersFilterAccountsForm } from "./MarketOffersFilterAccountsForm";
export default {
title: "organisms/MarketOffersFilterAccountsForm",
component: MarketOffersFilterAccountsForm,
} as ComponentMeta<typeof MarketOffersFilterAccountsForm>;
const Template: ComponentStory<typeof MarketOffersFilterAccountsForm> = () => {
return <MarketOffersFilterAccountsForm />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,197 @@
// =============================================================================
// 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 { FormattedMessage } from "react-intl";
import { Grid, Text, Checkbox, createStyles, Group } from "@mantine/core";
import { useForm } from "@mantine/form";
import { Button, TextButton } from "@atoms/Buttons";
import { NumberInput } from "@atoms/TextInput";
import { LangKeys } from "@constants/lang";
import { useOffersFilterState } from "@src/state/offersFilter";
import { transformToForm } from "@src/utils/misc";
interface MarketOffersFilterAccountsFormProps {
onSubmit?: (values: MarketOffersFilterAccountsForm) => void;
}
export function MarketOffersFilterAccountsForm({
onSubmit,
}: MarketOffersFilterAccountsFormProps) {
const { classes } = useStyles();
const [offersState, setOffersState] = useOffersFilterState();
const form = useForm<MarketOffersFilterAccountsForm>({
initialValues: {
...initialValues,
// We only care about the fields in the form and remove other fields.
// Previously unfilled optional values come as null, so remove those as well.
...transformToForm(offersState, initialValues),
},
});
const handleClearFilter = () => {
form.setValues({ ...initialValues });
};
return (
<form
onSubmit={form.onSubmit((values) => {
setOffersState((oldFilter) => ({
...oldFilter,
...values,
}));
onSubmit && onSubmit(values);
})}
>
<Grid mb="xl">
<Grid.Col span={8}>
<Text weight={500}>
<FormattedMessage
id={LangKeys.MarketFilterAccountLabelSignedAccounts}
defaultMessage="Signed Accounts"
/>
</Text>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketFilterAccountDescSignedAccounts}
defaultMessage="Only show accounts that have been signed. Please be aware that new accounts need to get the chance to get signed."
/>
</Text>
</Grid.Col>
<Grid.Col sx={{ display: "flex" }} span={4}>
<Checkbox
id="signedAccounts"
radius="sm"
size="md"
sx={{ alignItems: "flex-start", marginLeft: "auto" }}
{...form.getInputProps("signedAccounts", { type: "checkbox" })}
/>
</Grid.Col>
</Grid>
<Grid mb="xl">
<Grid.Col span={8}>
<Text weight={500}>
<FormattedMessage
id={LangKeys.MarketFilterAccountLabelMinAccountAge}
defaultMessage="Minimum account age"
/>
</Text>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketFilterAccountDescMinAccountAge}
defaultMessage="Only show trade offers with a minimum account age."
/>
</Text>
</Grid.Col>
<Grid.Col span={4}>
<NumberInput
id="minimumAccountAge"
{...form.getInputProps("minimumAccountAge")}
rightSection={
<Text pr="md" color="gray">
<FormattedMessage
id={LangKeys.MarketFilterAccountDays}
defaultMessage="Days"
/>
</Text>
}
rightSectionWidth={50}
/>
</Grid.Col>
</Grid>
<Grid>
<Grid.Col span={8}>
<Text weight={500}>
<FormattedMessage
id={LangKeys.MarketFilterAccountLabelAmountTrades}
defaultMessage="Minimum amount of trades"
/>
</Text>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketFilterAccountDescAmountTrades}
defaultMessage="Only show trade offers from accounts with a minimum amount of completed trades"
/>
</Text>
</Grid.Col>
<Grid.Col span={4}>
<NumberInput
id="minimumTradesAmount"
{...form.getInputProps("minimumTradesAmount")}
rightSection={
<Text mr="md" color="gray">
<FormattedMessage
id={LangKeys.MarketFilterAccountTrades}
defaultMessage="Trades"
/>
</Text>
}
rightSectionWidth={65}
/>
</Grid.Col>
</Grid>
<Group position="apart" className={classes.footer}>
<TextButton
onClick={handleClearFilter}
className={classes.clearFilterBtn}
>
<FormattedMessage
id={LangKeys.MarketFilterAccountClearFiltersBtn}
defaultMessage="Clear filters"
/>
</TextButton>
<Button type="submit" flavor="primary">
<FormattedMessage
id={LangKeys.MarketFilterAccountClearFiltersBtn}
defaultMessage="Save filters"
/>
</Button>
</Group>
</form>
);
}
interface MarketOffersFilterAccountsForm {
signedAccounts: boolean;
minimumTradesAmount?: number | null;
minimumAccountAge?: number | null;
}
const useStyles = createStyles((theme) => ({
footer: {
paddingTop: theme.spacing.xl,
paddingLeft: theme.spacing.xl,
paddingRight: theme.spacing.xl,
borderTop: `1px solid ${theme.colors.gray[1]}`,
marginTop: theme.spacing.xl,
marginLeft: theme.spacing.xl * -1,
marginRight: theme.spacing.xl * -1,
},
clearFilterBtn: {
fontSize: theme.spacing.lg,
},
}));
const initialValues = {
signedAccounts: false,
minimumAccountAge: undefined,
minimumTradesAmount: undefined,
};

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 "./MarketOffersFilterAccountsForm";

View File

@ -0,0 +1,30 @@
// =============================================================================
// 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 { MarketOffersFilterAmountForm } from "./MarketOffersFilterAmountForm";
export default {
title: "organisms/MarketOffersFilterAmountForm",
component: MarketOffersFilterAmountForm,
} as ComponentMeta<typeof MarketOffersFilterAmountForm>;
const Template: ComponentStory<typeof MarketOffersFilterAmountForm> = () => {
return <MarketOffersFilterAmountForm />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,188 @@
// =============================================================================
// 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 { FormattedMessage } from "react-intl";
import { createStyles, Grid, Group, Text } from "@mantine/core";
import { useForm } from "@mantine/hooks";
import { NumberInput } from "@atoms/TextInput";
import { Button, TextButton } from "@atoms/Buttons";
import { useOffersFilterState } from "@src/state/offersFilter";
import { transformToForm } from "@utils/misc";
import { LangKeys } from "@constants/lang";
interface MarketOffersFilterAmountFormProps {
onSubmit?: (values: MarketOffersFilterAmountFormValues) => void;
}
export function MarketOffersFilterAmountForm({
onSubmit,
}: MarketOffersFilterAmountFormProps) {
const { classes } = useStyles();
const [offersState, setOffersState] = useOffersFilterState();
const form = useForm<MarketOffersFilterAmountFormValues>({
initialValues: {
...initialValues,
// We only care about the fields in the form and remove other fields.
// Previously unfilled optional values come as null, so remove those as well.
...transformToForm(offersState, initialValues),
},
});
const handleCreateFilter = () => {
form.setValues({ ...initialValues });
};
return (
<form
onSubmit={form.onSubmit((values) => {
setOffersState((oldFilter) => ({
...oldFilter,
...values,
}));
onSubmit && onSubmit(values);
})}
>
<Grid>
<Grid.Col span={8}>
<Text weight={500}>
<FormattedMessage
id={LangKeys.MarketAmountFilterFieldMinAmountTrades}
defaultMessage="Minimum amount of trades"
/>
</Text>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketAmountFilterFieldMinAmountTradesDesc}
defaultMessage="Set the minimum amount you want to buy."
/>
</Text>
</Grid.Col>
<Grid.Col span={4}>
<NumberInput
id="minAmountFrom"
rightSection={
<Text color="gray" pr="sm">
EUR
</Text>
}
rightSectionWidth={45}
mb="lg"
{...form.getInputProps("minimumBaseCurrencyAmount")}
/>
<NumberInput
id="minAmountTo"
rightSection={
<Text pr="sm" color="gray">
XMR
</Text>
}
rightSectionWidth={45}
{...form.getInputProps("minimumCryptoAmount")}
/>
</Grid.Col>
</Grid>
<Grid mt="xl">
<Grid.Col span={8}>
<Text weight={500}>
<FormattedMessage
id={LangKeys.MarketAmountFilterFieldMaxAmount}
defaultMessage="Maximum amount"
/>
</Text>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketAmountFilterFieldMaxAmountDesc}
defaultMessage="Set the maximum amount you want to buy."
/>
</Text>
</Grid.Col>{" "}
<Grid.Col span={4}>
<NumberInput
id="maxAmountFrom"
{...form.getInputProps("maximumCryptoAmount")}
rightSection={
<Text pr="sm" color="gray">
XMR
</Text>
}
rightSectionWidth={45}
mb="lg"
/>
<NumberInput
id="maxAmountTo"
{...form.getInputProps("maximumBaseCurrencyAmount")}
rightSection={
<Text pr="sm" color="gray">
EUR
</Text>
}
rightSectionWidth={45}
/>
</Grid.Col>
</Grid>
<Group position="apart" className={classes.footer}>
<TextButton
onClick={handleCreateFilter}
className={classes.clearFilterBtn}
>
<FormattedMessage
id={LangKeys.MarketAmountFilterAmountClearFiltersBtn}
defaultMessage="Clear filters"
/>
</TextButton>
<Button type="submit" flavor="primary">
<FormattedMessage
id={LangKeys.MarketAmountFilterAmountSaveBtn}
defaultMessage="Save filters"
/>
</Button>
</Group>
</form>
);
}
const useStyles = createStyles((theme) => ({
footer: {
borderTop: `1px solid ${theme.colors.gray[1]}`,
marginLeft: theme.spacing.xl * -1,
marginRight: theme.spacing.xl * -1,
marginTop: theme.spacing.xl,
paddingLeft: theme.spacing.xl,
paddingRight: theme.spacing.xl,
paddingTop: theme.spacing.xl,
},
clearFilterBtn: {
fontSize: theme.spacing.lg,
},
}));
interface MarketOffersFilterAmountFormValues {
minimumCryptoAmount?: number | null;
minimumBaseCurrencyAmount?: number | null;
maximumCryptoAmount?: number | null;
maximumBaseCurrencyAmount?: number | null;
}
const initialValues = {
minimumCryptoAmount: undefined,
minimumBaseCurrencyAmount: undefined,
maximumCryptoAmount: undefined,
maximumBaseCurrencyAmount: undefined,
};

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 "./MarketOffersFilterAmountForm";

View File

@ -0,0 +1,30 @@
// =============================================================================
// 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 { MarketOffersFilterBar } from ".";
export default {
title: "organisms/MarketOffersFilterBar",
component: MarketOffersFilterBar,
} as ComponentMeta<typeof MarketOffersFilterBar>;
const Template: ComponentStory<typeof MarketOffersFilterBar> = () => {
return <MarketOffersFilterBar />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,200 @@
// =============================================================================
// 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 { Divider, Group, createStyles, Text, Box } from "@mantine/core";
import { FormattedMessage, useIntl } from "react-intl";
import { isEmpty } from "lodash";
import {
useMarketOffersPairModal,
useMarketOffersPaymentMethods,
useMarketOffersAmountModal,
useMarketOffersAccountModal,
} from "./hooks";
import { MarketOffersFilterButton } from "./MarketOffersFilterButton";
import {
useAccountDetailsLabel,
useMarketOffersFilterAmountLabel,
} from "./_hooks";
import { LangKeys } from "@constants/lang";
import { ReactComponent as BtcIcon } from "@assets/btc.svg";
import { useOffersFilterState } from "@src/state/offersFilter";
import { ToggleButton } from "@atoms/ToggleButton/ToggleButton";
export function MarketOffersFilterBar() {
const { formatMessage } = useIntl();
const { classes } = useStyles();
const [offersFilter, setOffersFilter] = useOffersFilterState();
// Market offers pair filter modal.
const marketOffersPairModal = useMarketOffersPairModal();
// Market offers payment methods filter modal.
const marketOffersPaymentMethodsModal = useMarketOffersPaymentMethods();
// Market offers account filter modal.
const marketOffersAccountModal = useMarketOffersAccountModal();
// Market offers amount filter modal.
const marketOffersAmountModal = useMarketOffersAmountModal();
const accountDetailsLabel = useAccountDetailsLabel();
const filterAmountLabel = useMarketOffersFilterAmountLabel();
// Handles the buy/sell switch change.
const handleBuySellSwitch = (tabIndex: number) => {
setOffersFilter((filter) => ({
...filter,
direction: tabIndex === 0 ? "sell" : "buy",
}));
};
const handleParisBtnClick = () => {
marketOffersPairModal.openModal();
};
const handlePaymentMethodsBtnClick = () => {
marketOffersPaymentMethodsModal.openModal();
};
const handleAccountBtnClick = () => {
marketOffersAccountModal.openModal();
};
const handleAmountBtnClick = () => {
marketOffersAmountModal.openModal();
};
return (
<Group position="apart" className={classes.root}>
<Group>
<Group spacing="sm">
<ToggleButton
labels={[
formatMessage(
{
id: LangKeys.MarketOffersSwitchSell,
defaultMessage: "Sell {currency}",
},
{
currency: "XMR",
}
),
formatMessage(
{
id: LangKeys.MarketOffersSwitchBuy,
defaultMessage: "Buy {currency}",
},
{
currency: "XMR",
}
),
]}
onChange={handleBuySellSwitch}
/>
<Text color="gray">
<FormattedMessage
id={LangKeys.MarketOffersWith}
defaultMessage="with"
/>
</Text>
<MarketOffersFilterButton
active={!isEmpty(offersFilter.assetCode)}
onClick={handleParisBtnClick}
>
<Box mr="sm">
<BtcIcon height={17} width={17} />
</Box>
{!isEmpty(offersFilter.assetCode) ? (
offersFilter.assetCode?.toUpperCase()
) : (
<FormattedMessage
id={LangKeys.MarketOffersCurrency}
defaultMessage="Currency"
/>
)}
</MarketOffersFilterButton>
</Group>
<Divider className={classes.divider} orientation="vertical" />
<MarketOffersFilterButton
active={!!filterAmountLabel}
onClick={handleAmountBtnClick}
>
{filterAmountLabel || (
<FormattedMessage
id={LangKeys.MarketOffersAmount}
defaultMessage="Amount"
/>
)}
</MarketOffersFilterButton>
<MarketOffersFilterButton
active={!isEmpty(offersFilter.paymentMethods)}
bubbleText={
isEmpty(offersFilter.paymentMethods)
? ""
: offersFilter?.paymentMethods?.length + ""
}
onClick={handlePaymentMethodsBtnClick}
>
<FormattedMessage
id={LangKeys.MarketOffersPaymentMethod}
defaultMessage="Payment method"
/>
</MarketOffersFilterButton>
<MarketOffersFilterButton
active={!!accountDetailsLabel}
onClick={handleAccountBtnClick}
>
{accountDetailsLabel || (
<FormattedMessage
id={LangKeys.MarketOffersAccountDetails}
defaultMessage="Account details"
/>
)}
</MarketOffersFilterButton>
</Group>
<Group>
<Divider className={classes.divider} orientation="vertical" />
<MarketOffersFilterButton>
<FormattedMessage
id={LangKeys.MarketOffersShowMarketDepth}
defaultMessage="Show market depth"
/>
</MarketOffersFilterButton>
<MarketOffersFilterButton variant="filled" color="blue" size="md">
<FormattedMessage
id={LangKeys.MarketOffersCreateOffer}
defaultMessage="Create Offer"
/>
</MarketOffersFilterButton>
</Group>
</Group>
);
}
const useStyles = createStyles((theme) => ({
root: {
background: theme.white,
borderBottom: `1px solid ${theme.colors.gray[3]}`,
minHeight: 84,
padding: "18px 22px",
},
divider: {
marginBottom: "auto",
marginTop: "auto",
height: 28,
},
}));

View File

@ -0,0 +1,98 @@
// =============================================================================
// 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 { ButtonProps as MButtonProps, ButtonVariant } from "@mantine/core";
import { Button, Box, createStyles } from "@mantine/core";
export interface ButtonProps extends MButtonProps<"button"> {
active?: boolean;
bubbleText?: string;
}
interface MarketOffersFilterButtonStyleProps {
active: boolean;
variant: ButtonVariant;
}
export function MarketOffersFilterButton(props: ButtonProps) {
const {
bubbleText,
className,
active,
variant = "outline",
classNames,
...others
} = props;
const { cx, classes } = useStyles(
{
active: active || false,
variant: variant,
},
{
name: "MarketOffersFilterButton",
classNames,
}
);
return (
<Button
color="gray"
radius="md"
size="md"
variant={variant}
{...others}
className={cx(
classes.root,
{
[classes.outline]: variant === "outline",
},
className
)}
>
{bubbleText && <Box className={classes.bubbleText}>{bubbleText}</Box>}
{props.children}
</Button>
);
}
const useStyles = createStyles(
(theme, { active }: MarketOffersFilterButtonStyleProps) => ({
root: {
paddingLeft: theme.spacing.sm,
paddingRight: theme.spacing.sm,
position: "relative",
},
outline: {
borderColor: active ? "#111" : "#E8E7EC",
borderWidth: active ? 2 : 1,
color: "#111",
},
bubbleText: {
backgroundColor: "#111",
borderRadius: 15,
boxShadow: "0 0 0 2px #fff",
color: "#fff",
position: "absolute",
padding: "2px 4px",
top: -5,
right: -5,
fontSize: 10,
lineHeight: 1,
minWidth: 15,
},
})
);

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 { useIntl } from "react-intl";
import { LangKeys } from "@constants/lang";
import { useOffersFilterState } from "@src/state/offersFilter";
/**
* Retrieve the active label of account details button.
* @returns {string}
*/
export function useAccountDetailsLabel() {
const { formatMessage } = useIntl();
const [offersFilterState] = useOffersFilterState();
return [
[
formatMessage({
id: LangKeys.MarketOffersSigned,
defaultMessage: "Signed",
}),
offersFilterState.signedAccounts,
],
[
formatMessage(
{
id: LangKeys.MarketOffersTradesAmount,
defaultMessage: ">{value} days",
},
{ value: offersFilterState.minimumTradesAmount }
),
offersFilterState.minimumTradesAmount,
],
[
formatMessage(
{
id: LangKeys.MarketOffersDaysAge,
defaultMessage: ">{value} days",
},
{
value: offersFilterState.minimumAccountAge,
}
),
offersFilterState.minimumAccountAge,
],
]
.filter((option) => option[1])
.map((option) => option[0])
.join(", ");
}
/**
* Retrieve the active label of amount button.
* @returns {string}
*/
export function useMarketOffersFilterAmountLabel() {
const [offersFilterState] = useOffersFilterState();
if (
!offersFilterState.minimumBaseCurrencyAmount &&
!offersFilterState.maximumBaseCurrencyAmount
) {
return "";
}
const fromAmount = offersFilterState.minimumBaseCurrencyAmount || "~";
const toAmount = offersFilterState.maximumBaseCurrencyAmount || "~";
return `${fromAmount} - ${toAmount} XMR`;
}

View File

@ -0,0 +1,20 @@
// =============================================================================
// 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 "./useMarketOffersPairModal";
export * from "./useMarketOffersPaymentMethods";
export * from "./useMarketOffersAccountModal";
export * from "./useMarketOffersAmountModal";

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 { useModals } from "@mantine/modals";
import { MarketOffersFilterAccountsForm } from "@organisms/MarketOffersFilterAccountsForm";
export function useMarketOffersAccountModal() {
const modals = useModals();
return {
openModal: () => {
const modalId = modals.openModal({
title: "Amount",
children: (
<MarketOffersFilterAccountsForm
onSubmit={() => {
modals.closeModal(modalId);
}}
/>
),
size: "lg",
withCloseButton: true,
});
},
};
}

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 { useModals } from "@mantine/modals";
import { MarketOffersFilterAmountForm } from "@organisms/MarketOffersFilterAmountForm";
export function useMarketOffersAmountModal() {
const modals = useModals();
return {
openModal: () => {
const modalId = modals.openModal({
title: "Amount",
children: (
<MarketOffersFilterAmountForm
onSubmit={() => {
modals.closeModal(modalId);
}}
/>
),
size: "lg",
withCloseButton: false,
});
},
};
}

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 { createStyles } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { MarketOffersTradingPair } from "@organisms/MarketOffersTradingPair";
export function useMarketOffersPairModal() {
const modals = useModals();
const { classes } = useStyles();
return {
openModal: () => {
const modalId = modals.openModal({
title: "Select trading pair",
children: (
<MarketOffersTradingPair
onSubmit={() => {
modals.closeModal(modalId);
}}
/>
),
withCloseButton: true,
size: 570,
classNames: classes,
});
},
};
}
const useStyles = createStyles((theme) => ({
title: {
fontSize: theme.fontSizes.md,
fontWeight: 600,
},
header: {
marginBottom: 10,
marginTop: -10,
},
body: {
marginLeft: theme.spacing.sm * -2,
marginRight: theme.spacing.sm * -2,
marginBottom: theme.spacing.sm * -2,
},
}));

View File

@ -0,0 +1,53 @@
// =============================================================================
// 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 { useModals } from "@mantine/modals";
import { MarketOffersFilterPaymentMethods } from "@organisms/MarketOffersFilterPaymentMethods";
export function useMarketOffersPaymentMethods() {
const modals = useModals();
const { classes } = useStyles();
return {
openModal: () => {
modals.openModal({
title: "Filter on payment methods",
children: <MarketOffersFilterPaymentMethods />,
size: 970,
withCloseButton: true,
classNames: classes,
});
},
};
}
const useStyles = createStyles((theme) => ({
root: {
padding: "0 !important",
},
modal: {
padding: "0 !important",
},
title: {
fontSize: theme.fontSizes.md,
fontWeight: 600,
},
header: {
padding: "12px 20px",
margin: 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 "./MarketOffersFilterBar";

View File

@ -0,0 +1,71 @@
// =============================================================================
// 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 { includes } from "lodash";
import type { FC } from "react";
import { useMemo } from "react";
import type { TMarketOfferPaymentMethod } from "@organisms/MarketOffersPaymentMethodsTable";
import { MarketOffersPaymentMethodsTable } from "@organisms/MarketOffersPaymentMethodsTable";
import { useOffersFilterState } from "@src/state/offersFilter";
import { usePaymentMethods } from "@hooks/haveno/usePaymentMethods";
export function MarketOffersFilterPaymentMethodsLoaded() {
const { data: paymentMethods } = usePaymentMethods();
const [filter, setFilter] = useOffersFilterState();
const tableData = useMemo(
() =>
paymentMethods?.map((item) => ({
...item,
methodChecked: includes(filter.paymentMethods, item.methodKey),
})),
[paymentMethods]
);
const handleEditableDataChange = (
newData: Array<TMarketOfferPaymentMethod>
) => {
setFilter((oldQuery) => ({
...oldQuery,
paymentMethods: newData
.filter((payment) => payment.methodChecked)
.map((payment) => payment.methodKey),
}));
};
if (!tableData) {
return null;
}
return (
<MarketOffersPaymentMethodsTable
data={tableData}
onEditableDataChange={handleEditableDataChange}
/>
);
}
const MarketOffersFilterPaymentMethodsBoot: FC = ({ children }) => {
const { isLoading } = usePaymentMethods();
return isLoading ? <>Loading</> : <>{children}</>;
};
export function MarketOffersFilterPaymentMethods() {
return (
<MarketOffersFilterPaymentMethodsBoot>
<MarketOffersFilterPaymentMethodsLoaded />
</MarketOffersFilterPaymentMethodsBoot>
);
}

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 "./MarketOffersFilterPaymentMethods";

View File

@ -0,0 +1,65 @@
// =============================================================================
// 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 type { TMarketOfferPaymentMethod } from "./_types";
import { MarketOffersPaymentMethodsTable } from ".";
export default {
title: "organisms/MarketOffersPaymentMethodsTable",
component: MarketOffersPaymentMethodsTable,
} as ComponentMeta<typeof MarketOffersPaymentMethodsTable>;
const Template: ComponentStory<typeof MarketOffersPaymentMethodsTable> = (
args
) => {
return <MarketOffersPaymentMethodsTable data={args.data} />;
};
export const Default = Template.bind({});
Default.args = {
data: [
{
methodName: "Celpay",
methodKey: "celpay",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "USA",
},
{
methodName: "ACH",
methodKey: "ach",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global (AUS, TRY, USD)",
},
{
methodName: "Cash by mail",
methodKey: "cash-by-mail",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Spain",
},
{
methodName: "Domestic Wire Transfer",
methodKey: "domestic-wire-transfer",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global",
},
] as Array<TMarketOfferPaymentMethod>,
};

View File

@ -0,0 +1,95 @@
// =============================================================================
// 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 type { TMarketOfferPaymentMethod } from "./_types";
import { MarketOffersPaymentMethodsTable } from ".";
import { AppProviders } from "@atoms/AppProviders";
describe("molecules::MarketOffersPaymentMethodsTable", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MarketOffersPaymentMethodsTable data={data} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all columns", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersPaymentMethodsTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Method")).toBeInTheDocument();
expect(screen.queryByText("Rate Trade Limit")).toBeInTheDocument();
expect(screen.queryByText("Info")).toBeInTheDocument();
unmount();
});
it("renders cells of method name ", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersPaymentMethodsTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Celpay")).toBeInTheDocument();
expect(screen.queryByText("ACH")).toBeInTheDocument();
unmount();
});
it("renders cells of rate trade limit ", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersPaymentMethodsTable data={data} />
</AppProviders>
);
expect(screen.queryByText("20 XMR")).toBeInTheDocument();
expect(screen.queryByText("40 XMR")).toBeInTheDocument();
unmount();
});
it("renders cells of rate trade limit ", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersPaymentMethodsTable data={data} />
</AppProviders>
);
expect(screen.queryByText("USA")).toBeInTheDocument();
expect(screen.queryByText("Global (AUS, TRY, USD)")).toBeInTheDocument();
unmount();
});
});
const data = [
{
methodName: "Celpay",
methodKey: "celpay",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "USA",
},
{
methodName: "ACH",
methodKey: "ach",
rateTradeLimit: 40,
rateTradeLimitCurrency: "XMR",
info: "Global (AUS, TRY, USD)",
},
] as Array<TMarketOfferPaymentMethod>;

View File

@ -0,0 +1,137 @@
// =============================================================================
// 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 { createTable } from "@tanstack/react-table";
import { createStyles } from "@mantine/core";
import { useIntl } from "react-intl";
import {
MarketOffersPaymentMethodsInfo,
MarketOffersPaymentMethodsLimit,
} from "./MarketOffersPaymentMethodsTableCells";
import type { TMarketOfferPaymentMethod } from "./_types";
import type { TableProps } from "@molecules/Table";
import { CheckboxCell, Table } from "@molecules/Table";
import { LangKeys } from "@constants/lang";
const table = createTable().setRowType<TMarketOfferPaymentMethod>();
interface MarketOffersPaymentMethodsTableProps extends Partial<TableProps> {
data: Array<TMarketOfferPaymentMethod>;
}
export function MarketOffersPaymentMethodsTable({
data,
...rest
}: MarketOffersPaymentMethodsTableProps) {
const { classes } = useStyles();
const columns = useMarketOffersPaymentMethodsColumns();
return (
<Table
{...rest}
table={table}
columns={columns}
data={data}
tableWrap={{
verticalSpacing: "xs",
striped: true,
className: classes.root,
}}
/>
);
}
const useMarketOffersPaymentMethodsColumns = () => {
const { formatMessage } = useIntl();
return [
table.createDataColumn("methodChecked", {
id: "methodChecked",
header: " ",
cell: (props) => (
<CheckboxCell {...props} checkboxProps={{ radius: "xs", size: "sm" }} />
),
size: 30,
}),
table.createDataColumn("methodName", {
id: "methodName",
header: formatMessage({
id: LangKeys.MarketPaymentMethodColMethodName,
defaultMessage: "Method",
}),
size: 300,
}),
table.createDataColumn("rateTradeLimit", {
id: "rateTradeLimit",
header: formatMessage({
id: LangKeys.MarketPaymentMethodColRateTradeLimit,
defaultMessage: "Rate Trade Limit",
}),
size: 400,
cell: ({ row }) => (
<MarketOffersPaymentMethodsLimit row={row?.original} />
),
}),
table.createDataColumn("info", {
id: "info",
header: formatMessage({
id: LangKeys.MarketPaymentMethodColInfo,
defaultMessage: "Info",
}),
size: 400,
cell: ({ row }) => <MarketOffersPaymentMethodsInfo row={row?.original} />,
meta: { textAlign: "right" },
}),
];
};
const useStyles = createStyles((theme) => ({
root: {
thead: {
tr: {
th: {
color: theme.colors.gray[9],
fontSize: theme.fontSizes.xs,
paddingBottom: 8,
paddingTop: 8,
textTransform: "uppercase",
"&:first-of-type": {
paddingLeft: theme.spacing.xl,
},
"&:last-of-type": {
paddingRight: theme.spacing.xl,
},
},
},
},
tbody: {
tr: {
td: {
borderBottom: 0,
fontSize: theme.fontSizes.md,
"&:first-of-type": {
paddingLeft: theme.spacing.xl,
},
"&:last-of-type": {
paddingRight: theme.spacing.xl,
},
},
},
},
},
}));

View File

@ -0,0 +1,44 @@
// =============================================================================
// 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 { Text } from "@mantine/core";
import type { TMarketOfferPaymentMethod } from "./_types";
import { Currency } from "@atoms/Currency";
export function MarketOffersPaymentMethodsLimit({
row,
}: {
row?: TMarketOfferPaymentMethod;
}) {
return (
<Text size="sm" color="gray">
<Currency value={row?.rateTradeLimit || 0} minimumFractionDigits={0} />{" "}
{row?.rateTradeLimitCurrency}
</Text>
);
}
export function MarketOffersPaymentMethodsInfo({
row,
}: {
row?: TMarketOfferPaymentMethod;
}) {
return (
<Text size="sm" color="gray">
{row?.info}
</Text>
);
}

View File

@ -0,0 +1,150 @@
// Vitest Snapshot v1
exports[`molecules::MarketOffersPaymentMethodsTable > renders without exploding 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root __mantine-ref-striped mantine-Table-striped mantine-6859xh"
>
<thead>
<tr>
<th
colspan="1"
style="width: 30px;"
>
</th>
<th
colspan="1"
style="width: 300px;"
>
Method
</th>
<th
colspan="1"
style="width: 400px;"
>
Rate Trade Limit
</th>
<th
colspan="1"
style="width: 400px; text-align: right;"
>
Info
</th>
</tr>
</thead>
<tbody>
<tr>
<td
style="width: 30px;"
>
<div
class="mantine-Checkbox-root mantine-16ttirm"
>
<div
class="mantine-qbjb2s mantine-Checkbox-inner"
>
<input
class="mantine-1mvrtfa mantine-Checkbox-input"
id="methodChecked-0"
type="checkbox"
/>
<svg
class="__mantine-ref-icon mantine-180iq9w mantine-Checkbox-icon"
fill="none"
viewBox="0 0 10 7"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M4 4.586L1.707 2.293A1 1 0 1 0 .293 3.707l3 3a.997.997 0 0 0 1.414 0l5-5A1 1 0 1 0 8.293.293L4 4.586z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
</td>
<td
style="width: 300px;"
>
Celpay
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-67rh7b"
>
20 XMR
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
<div
class="mantine-Text-root mantine-67rh7b"
>
USA
</div>
</td>
</tr>
<tr>
<td
style="width: 30px;"
>
<div
class="mantine-Checkbox-root mantine-16ttirm"
>
<div
class="mantine-qbjb2s mantine-Checkbox-inner"
>
<input
class="mantine-1mvrtfa mantine-Checkbox-input"
id="methodChecked-1"
type="checkbox"
/>
<svg
class="__mantine-ref-icon mantine-180iq9w mantine-Checkbox-icon"
fill="none"
viewBox="0 0 10 7"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M4 4.586L1.707 2.293A1 1 0 1 0 .293 3.707l3 3a.997.997 0 0 0 1.414 0l5-5A1 1 0 1 0 8.293.293L4 4.586z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</div>
</div>
</td>
<td
style="width: 300px;"
>
ACH
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Text-root mantine-67rh7b"
>
40 XMR
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
<div
class="mantine-Text-root mantine-67rh7b"
>
Global (AUS, TRY, USD)
</div>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -0,0 +1,24 @@
// =============================================================================
// 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 TMarketOfferPaymentMethod {
methodChecked?: boolean;
methodName: string;
methodKey: string;
rateTradeLimit: number;
rateTradeLimitCurrency: string;
info: string;
}

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 "./MarketOffersPaymentMethodsTable";
export type { TMarketOfferPaymentMethod } from "./_types";

View File

@ -0,0 +1,76 @@
// =============================================================================
// 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 { useCallback } from "react";
import type { FC } from "react";
import type { Row } from "@tanstack/react-table";
import type { TMarketTradingPairTable } from "@organisms/MarketOffersTradingPairTable";
import { MarketOffersTradingPairTable } from "@organisms/MarketOffersTradingPairTable";
import { useOffersFilterState } from "@src/state/offersFilter";
import { useMarketsPairs } from "@hooks/haveno/useMarketPairs";
interface MarketOffersTradingPairProps {
onSubmit?: (row: Row<TMarketTradingPairTable>) => void;
}
export function MarketOffersTradingPair({
onSubmit,
}: MarketOffersTradingPairProps) {
return (
<MarketOffersTradingPairBoot>
<MarketOffersTradingPairLoaded onSubmit={onSubmit} />
</MarketOffersTradingPairBoot>
);
}
interface MarketOffersTradingPairLoadedProps {
onSubmit?: (row: Row<TMarketTradingPairTable>) => void;
}
function MarketOffersTradingPairLoaded({
onSubmit,
}: MarketOffersTradingPairLoadedProps) {
const { data: marketsPairs } = useMarketsPairs();
const [, setOffersState] = useOffersFilterState();
const handleRowClick = useCallback(
(row: Row<TMarketTradingPairTable>) => {
// Sync the selected pair to atom global filter state.
setOffersState((oldFilter) => ({
...oldFilter,
assetCode: row.original?.fromPair || "",
}));
onSubmit && onSubmit(row);
},
[onSubmit]
);
if (!marketsPairs) {
return null;
}
return (
<MarketOffersTradingPairTable
data={marketsPairs}
onRowClick={handleRowClick}
/>
);
}
const MarketOffersTradingPairBoot: FC = ({ children }) => {
const { isLoading } = useMarketsPairs();
return isLoading ? <>Loading</> : <>{children}</>;
};

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 "./MarketOffersTradingPair";

View File

@ -0,0 +1,69 @@
// =============================================================================
// 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 { MarketOffersTradingPairTable } from "./MarketOffersTradingPairTable";
import type { TMarketOffersTradingPair } from "./_types";
export default {
title: "organisms/MarketOffersTradingPairTable",
component: MarketOffersTradingPairTable,
} as ComponentMeta<typeof MarketOffersTradingPairTable>;
const Template: ComponentStory<typeof MarketOffersTradingPairTable> = (
args
) => {
return <MarketOffersTradingPairTable data={args.data} />;
};
export const Default = Template.bind({});
Default.args = {
data: [
{
fromPair: "EUR",
toPair: "XMR",
lastPrice: 101.122,
lastPriceCurrency: "EUR",
dayChangeRate: 12.12,
dayChangeVolume: 1222.123,
},
{
fromPair: "EUR",
toPair: "XMR",
lastPrice: 101.122,
lastPriceCurrency: "EUR",
dayChangeRate: 12.12,
dayChangeVolume: 1222.123,
},
{
fromPair: "EUR",
toPair: "XMR",
lastPrice: 101.122,
lastPriceCurrency: "EUR",
dayChangeRate: 12.12,
dayChangeVolume: 1222.123,
},
{
fromPair: "EUR",
toPair: "XMR",
lastPrice: 101.122,
lastPriceCurrency: "EUR",
dayChangeRate: 12.12,
dayChangeVolume: 1222.123,
},
] as Array<TMarketOffersTradingPair>,
};

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 { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import type { TMarketOffersTradingPair } from "./_types";
import { MarketOffersTradingPairTable } from "./MarketOffersTradingPairTable";
import { AppProviders } from "@atoms/AppProviders";
describe("molecules::MarketoffersTradingPairTable", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<MarketOffersTradingPairTable data={data} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("renders all columns", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTradingPairTable data={data} />
</AppProviders>
);
expect(screen.queryByText("Pair")).toBeInTheDocument();
expect(screen.queryByText("Last Price")).toBeInTheDocument();
expect(screen.queryByText("24th Change")).toBeInTheDocument();
expect(screen.queryByText("24th Vol")).toBeInTheDocument();
unmount();
});
it("renders Pair cells", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTradingPairTable data={data} />
</AppProviders>
);
expect(screen.queryByText("EUR/XMR")).toBeInTheDocument();
expect(screen.queryByText("USD/XMR")).toBeInTheDocument();
unmount();
});
it("renders Day Change Volume cells", () => {
const { unmount } = render(
<AppProviders>
<MarketOffersTradingPairTable data={data} />
</AppProviders>
);
expect(screen.queryByText("4,233.123")).toBeInTheDocument();
expect(screen.queryByText("1,222.123")).toBeInTheDocument();
unmount();
});
});
const data = [
{
fromPair: "EUR",
toPair: "XMR",
lastPrice: 101.12,
lastPriceCurrency: "EUR",
dayChangeRate: 12.12,
dayChangeVolume: 4233.123,
},
{
fromPair: "USD",
toPair: "XMR",
lastPrice: 105.12,
lastPriceCurrency: "EUR",
dayChangeRate: 83.12,
dayChangeVolume: 1222.123,
},
] as Array<TMarketOffersTradingPair>;

View File

@ -0,0 +1,143 @@
// =============================================================================
// 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 { useIntl } from "react-intl";
import { createStyles } from "@mantine/core";
import type { TMarketOffersTradingPair } from "./_types";
import { marketTradingPairTable } from "./_types";
import {
MarketOfferPair24thChange,
MarketOfferPairLastPriceCell,
MarketOfferPair24thChangeVolume,
} from "./MarketOffersTradingPairTableCells";
import { pairColumnAccessor } from "./_utils";
import type { TableProps } from "@molecules/Table";
import { Table } from "@molecules/Table";
import { LangKeys } from "@constants/lang";
export interface MarketOffersTradingPairTableProps extends Partial<TableProps> {
data: Array<TMarketOffersTradingPair>;
}
export function MarketOffersTradingPairTable({
data,
...rest
}: MarketOffersTradingPairTableProps) {
const { classes } = useStyles();
const columns = useMarketTradingPairsColumns();
return (
<Table
{...rest}
table={marketTradingPairTable}
columns={columns}
data={data}
tableWrap={{
verticalSpacing: "md",
highlightOnHover: true,
className: classes.root,
}}
/>
);
}
const useMarketTradingPairsColumns = () => {
const { formatMessage } = useIntl();
return [
marketTradingPairTable.createDataColumn(pairColumnAccessor, {
id: "pair",
header: formatMessage({
id: LangKeys.MarketTradingPairColPair,
defaultMessage: "Pair",
}),
size: 400,
}),
marketTradingPairTable.createDataColumn("lastPrice", {
id: "lastPrice",
header: formatMessage({
id: LangKeys.MarketTradingPairColLastPrice,
defaultMessage: "Last Price",
}),
size: 400,
cell: ({ row }) => <MarketOfferPairLastPriceCell row={row?.original} />,
}),
marketTradingPairTable.createDataColumn("dayChangeRate", {
id: "dayChangeRate",
header: formatMessage({
id: LangKeys.MarketTradingPairColDayChange,
defaultMessage: "24th Change",
}),
size: 400,
cell: () => <MarketOfferPair24thChange />,
meta: { textAlign: "right" },
}),
marketTradingPairTable.createDataColumn("dayChangeVolume", {
id: "dayChangeVolume",
header: formatMessage({
id: LangKeys.MarketTradingPairColDayChangeVolume,
defaultMessage: "24h Vol",
}),
size: 400,
cell: ({ row }) => (
<MarketOfferPair24thChangeVolume row={row?.original} />
),
meta: { textAlign: "right" },
}),
];
};
const useStyles = createStyles((theme) => ({
root: {
paddingTop: 20,
paddingBottom: 0,
thead: {
tr: {
th: {
color: theme.colors.gray[9],
fontSize: theme.fontSizes.xs,
paddingTop: 8,
paddingBottom: 8,
textTransform: "uppercase",
"&:first-of-type": {
paddingLeft: theme.spacing.xl,
},
"&:last-of-type": {
paddingRight: theme.spacing.xl,
},
},
},
},
tbody: {
tr: {
td: {
borderBottomColor: "transparent",
fontSize: theme.spacing.sm * 1.168,
fontWeight: 600,
"&:first-of-type": {
paddingLeft: theme.spacing.xl,
},
"&:last-of-type": {
paddingRight: theme.spacing.xl,
},
},
},
},
},
}));

View File

@ -0,0 +1,49 @@
// =============================================================================
// 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 { Box, Group } from "@mantine/core";
import type { TMarketOffersTradingPair } from "./_types";
import { AmountChange } from "@atoms/AmountChange/AmountChange";
import { Currency } from "@atoms/Currency";
export function MarketOfferPairLastPriceCell({
row,
}: {
row?: TMarketOffersTradingPair;
}) {
return (
<Group spacing="md">
<Box>{row?.lastPriceCurrency}</Box>
<Box>
<Currency value={row?.lastPrice || 0} minimumFractionDigits={0} />
</Box>
</Group>
);
}
export function MarketOfferPair24thChange() {
return <AmountChange positive={true}>+3,5%</AmountChange>;
}
export function MarketOfferPair24thChangeVolume({
row,
}: {
row?: TMarketOffersTradingPair;
}) {
return (
<Currency value={row?.dayChangeVolume || 0} minimumFractionDigits={0} />
);
}

View File

@ -0,0 +1,118 @@
// Vitest Snapshot v1
exports[`molecules::MarketoffersTradingPairTable > renders without exploding 1`] = `
<DocumentFragment>
<table
class="mantine-Table-root __mantine-ref-hover mantine-Table-hover mantine-1u8kzyn"
>
<thead>
<tr>
<th
colspan="1"
style="width: 400px;"
>
Pair
</th>
<th
colspan="1"
style="width: 400px;"
>
Last Price
</th>
<th
colspan="1"
style="width: 400px; text-align: right;"
>
24th Change
</th>
<th
colspan="1"
style="width: 400px; text-align: right;"
>
24th Vol
</th>
</tr>
</thead>
<tbody>
<tr>
<td
style="width: 400px;"
>
EUR/XMR
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-6y1794"
>
<div
class="mantine-Group-child mantine-1oprzqz"
>
EUR
</div>
<div
class="mantine-Group-child mantine-1oprzqz"
>
101.12
</div>
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
<div
class="mantine-ozfbol"
>
+3,5%
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
4,233.123
</td>
</tr>
<tr>
<td
style="width: 400px;"
>
USD/XMR
</td>
<td
style="width: 400px;"
>
<div
class="mantine-Group-root mantine-6y1794"
>
<div
class="mantine-Group-child mantine-1oprzqz"
>
EUR
</div>
<div
class="mantine-Group-child mantine-1oprzqz"
>
105.12
</div>
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
<div
class="mantine-ozfbol"
>
+3,5%
</div>
</td>
<td
style="width: 400px; text-align: right;"
>
1,222.123
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

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 { createTable } from "@tanstack/react-table";
export interface TMarketOffersTradingPair {
fromPair: string;
toPair: string;
lastPrice: number;
lastPriceCurrency: string;
dayChangeRate: number;
dayChangeVolume: number;
}
export const marketTradingPairTable =
createTable().setRowType<TMarketOffersTradingPair>();
export type TMarketTradingPairTable = typeof marketTradingPairTable.generics;

View File

@ -0,0 +1,20 @@
// =============================================================================
// 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 { TMarketOffersTradingPair } from "./_types";
export const pairColumnAccessor = (row: TMarketOffersTradingPair): string =>
`${row.fromPair}/${row.toPair}`;

View File

@ -0,0 +1,21 @@
// =============================================================================
// 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 "./MarketOffersTradingPairTable";
export type {
TMarketOffersTradingPair,
TMarketTradingPairTable,
} from "./_types";

View File

@ -33,7 +33,7 @@ export const NAV_LINKS = [
{
icon: <MarketsIcon />,
label: "Markets",
link: "/markets",
route: "/markets",
},
{
icon: <OffersIcon />,

View File

@ -99,11 +99,53 @@ 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",
MarketsTransactionsColumnPrice = "marketsTransactions.columnPrice",
MarketsTransactionsColumnAmount = "marketsTransactions.columnAmount",
MarketsTransactionsColumnCost = "marketsTransactions.columnCost",
MarketsTransactionsColumnPaymentMethod = "marketsTransactions.columnPaymentMethod",
MarketsTransactionsColumnAccountAge = "marketsTransactions.columnAccountAge",
MarketsTransactionsColumnAccountTrades = "marketsTransactions.columnAccountTypes",
MarketsTransactionsCashByMail = "marketsTransactions.cashByMail",
MarketOffersAmount = "marketOffers.filter.amount",
MarketOffersWith = "marketOffers.filter.with",
MarketOffersSwitchSell = "marketOffers.filter.switchSellBuy.sell",
MarketOffersSwitchBuy = "marketOffers.filter.switchSellBuy.buy",
MarketOffersPaymentMethod = "marketOffers.filter.paymentMethod",
MarketOffersAccountDetails = "marketOffers.filter.accountDetails",
MarketOffersShowMarketDepth = "marketOffers.filter.showMarketDepth",
MarketOffersHideMarketDepth = "marketOffers.filter.hideMarketDepth",
MarketOffersCreateOffer = "marketOffers.filter.createOffer",
MarketOffersCurrency = "marketOffers.filter.currency",
MarketOffersSigned = "marketOffers.filter.signed",
MarketOffersTradesAmount = "marketOffers.filter.tradesAmount",
MarketOffersDaysAge = "marketOffers.filter.daysAge",
MarketPaymentMethodColMethodName = "marketOffers.filter.paymentMethodColMethodName",
MarketPaymentMethodColRateTradeLimit = "marketOffers.filter.paymentMethodColRateTradeLimit",
MarketPaymentMethodColInfo = "marketOffers.filter.paymentMethodColInfo",
MarketTradingPairColPair = "marketOffers.tradingPairFilter.pairColumn",
MarketTradingPairColLastPrice = "marketOffers.tradingPairFilter.lastPriceColumn",
MarketTradingPairColDayChange = "marketOffers.tradingPairFilter.dayChangeColumn",
MarketTradingPairColDayChangeVolume = "marketOffers.tradingPairFilter.dayChangeVolumColumn",
MarketFilterAccountLabelSignedAccounts = "marketFilters.accountFilter.labelSignedAccounts",
MarketFilterAccountDescSignedAccounts = "marketFilters.accountFilter.descSignedAccounts",
MarketFilterAccountLabelMinAccountAge = "marketFilters.accountFilter.labelMinAccountAge",
MarketFilterAccountDescMinAccountAge = "marketFilters.accountFilter.descMinAccountAge",
MarketFilterAccountLabelAmountTrades = "marketFilters.accountFilter.labelAmountTrades",
MarketFilterAccountDescAmountTrades = "marketFilters.accountFilter.descAmountTrades",
MarketFilterAccountTrades = "marketFilters.accountFilter.trades",
MarketFilterAccountDays = "marketFilters.accountFilter.days",
MarketFilterAccountClearFiltersBtn = "marketFilters.accountFilter.clearFiltersBtn",
MarketFilterAccountSaveBtn = "marketFilters.accountFilter.saveBtn",
MarketAmountFilterFieldMinAmountTrades = "MarketFilterFieldMinAmountTrades",
MarketAmountFilterFieldMinAmountTradesDesc = "MarketFilterFieldMinAmountTradesDesc",
MarketAmountFilterFieldMaxAmount = "MarketFilterFieldMaxAmount",
MarketAmountFilterFieldMaxAmountDesc = "MarketFilterFieldMaxAmountDesc",
MarketAmountFilterAmountClearFiltersBtn = "MarketFilterAmountClearFiltersBtn",
MarketAmountFilterAmountSaveBtn = "MarketFilterAmountSaveBtn",
}

View File

@ -122,6 +122,54 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.MarketsOffersColumnAccountAge]: "Account Age",
[LangKeys.MarketsOffersColumnAccountTrades]: "Account Trades",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Payment Method",
[LangKeys.MarketsTransactionsColumnPrice]: "Price",
[LangKeys.MarketsTransactionsColumnAmount]: "Amount",
[LangKeys.MarketsTransactionsColumnCost]: "Costs",
[LangKeys.MarketsTransactionsColumnAccountAge]: "Account Age",
[LangKeys.MarketsTransactionsColumnAccountTrades]: "Account Trades",
[LangKeys.MarketsTransactionsColumnPaymentMethod]: "Payment Method",
[LangKeys.MarketsTransactionsCashByMail]: "Cash by mail",
[LangKeys.MarketOffersAmount]: "Amount",
[LangKeys.MarketOffersWith]: "with",
[LangKeys.MarketOffersSwitchSell]: "Sell {currency}",
[LangKeys.MarketOffersSwitchBuy]: "Buy {currency}",
[LangKeys.MarketOffersPaymentMethod]: "Payment method",
[LangKeys.MarketOffersAccountDetails]: "Account details",
[LangKeys.MarketOffersShowMarketDepth]: "Show market depth",
[LangKeys.MarketOffersHideMarketDepth]: "Hide market depth",
[LangKeys.MarketOffersCreateOffer]: "Create offer",
[LangKeys.MarketOffersCurrency]: "Currency",
[LangKeys.MarketOffersSigned]: "Signed",
[LangKeys.MarketOffersTradesAmount]: ">{value} trades",
[LangKeys.MarketOffersDaysAge]: ">{value} days",
[LangKeys.MarketPaymentMethodColMethodName]: "Method",
[LangKeys.MarketPaymentMethodColRateTradeLimit]: "Rate Trade Limit",
[LangKeys.MarketPaymentMethodColInfo]: "Info",
[LangKeys.MarketTradingPairColPair]: "Pair",
[LangKeys.MarketTradingPairColLastPrice]: "Last Price",
[LangKeys.MarketTradingPairColDayChange]: "24th Change",
[LangKeys.MarketTradingPairColDayChangeVolume]: "24th Vol",
[LangKeys.MarketFilterAccountLabelSignedAccounts]: "Signed accounts",
[LangKeys.MarketFilterAccountDescSignedAccounts]:
"Only show accounts that have been signed. Please be aware that new accounts need to get the chance to get signed.",
[LangKeys.MarketFilterAccountLabelMinAccountAge]: "Minimum account age",
[LangKeys.MarketFilterAccountDescMinAccountAge]:
"Only show trade offers with a minimum account age.",
[LangKeys.MarketFilterAccountLabelAmountTrades]: "Minimum amount of trades",
[LangKeys.MarketFilterAccountDescAmountTrades]:
"Only show trade offers from accounts with a minimum amount of completed trades",
[LangKeys.MarketFilterAccountTrades]: "Trades",
[LangKeys.MarketFilterAccountDays]: "Days",
[LangKeys.MarketFilterAccountClearFiltersBtn]: "Clear filters",
[LangKeys.MarketFilterAccountSaveBtn]: "Save filters",
[LangKeys.MarketAmountFilterFieldMinAmountTrades]: "Minimum amount of trades",
[LangKeys.MarketAmountFilterFieldMinAmountTradesDesc]:
"Set the minimum amount you want to buy.",
[LangKeys.MarketAmountFilterFieldMaxAmount]: "Maximum amount",
[LangKeys.MarketAmountFilterFieldMaxAmountDesc]:
"Set the maximum amount you want to buy.",
[LangKeys.MarketAmountFilterAmountClearFiltersBtn]: "Clear filters",
[LangKeys.MarketAmountFilterAmountSaveBtn]: "Save filters",
};
export default LangPackEN;

View File

@ -125,6 +125,56 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.MarketsOffersColumnAccountAge]: "Edad de la cuenta",
[LangKeys.MarketsOffersColumnAccountTrades]: "Operaciones de cuenta",
[LangKeys.MarketsOffersColumnPaymentMethod]: "Método de pago",
[LangKeys.MarketsTransactionsColumnPrice]: "Precio",
[LangKeys.MarketsTransactionsColumnAmount]: "Monto",
[LangKeys.MarketsTransactionsColumnCost]: "Costos",
[LangKeys.MarketsTransactionsColumnAccountAge]: "Edad de la cuenta",
[LangKeys.MarketsTransactionsColumnAccountTrades]: "Operaciones de cuenta",
[LangKeys.MarketsTransactionsColumnPaymentMethod]: "Método de pago",
[LangKeys.MarketsTransactionsCashByMail]: "Cash by mail",
[LangKeys.MarketOffersAmount]: "Monto",
[LangKeys.MarketOffersWith]: "con",
[LangKeys.MarketOffersSwitchSell]: "Vender {currency}",
[LangKeys.MarketOffersSwitchBuy]: "Comprar {currency}",
[LangKeys.MarketOffersPaymentMethod]: "Método de pago",
[LangKeys.MarketOffersAccountDetails]: "Detalles de la cuenta",
[LangKeys.MarketOffersShowMarketDepth]: "Mostrar profundidad de mercado",
[LangKeys.MarketOffersHideMarketDepth]: "Ocultar profundidad de mercado",
[LangKeys.MarketOffersCreateOffer]: "Crear oferta",
[LangKeys.MarketOffersCurrency]: "Divisa",
[LangKeys.MarketOffersSigned]: "Firmada",
[LangKeys.MarketOffersTradesAmount]: ">{value} vientos alisios",
[LangKeys.MarketOffersDaysAge]: ">{value} días",
[LangKeys.MarketPaymentMethodColMethodName]: "Método",
[LangKeys.MarketPaymentMethodColRateTradeLimit]: "Límite comercial de tasa",
[LangKeys.MarketPaymentMethodColInfo]: "Información",
[LangKeys.MarketTradingPairColPair]: "Par",
[LangKeys.MarketTradingPairColLastPrice]: "Last Price",
[LangKeys.MarketTradingPairColDayChange]: "Cambio 24",
[LangKeys.MarketTradingPairColDayChangeVolume]: "Vol. 24",
[LangKeys.MarketFilterAccountLabelSignedAccounts]: "cuentas firmadas",
[LangKeys.MarketFilterAccountDescSignedAccounts]:
"Solo mostrar cuentas que hayan sido firmadas. Tenga en cuenta que las cuentas nuevas deben tener la oportunidad de ser firmadas.",
[LangKeys.MarketFilterAccountLabelMinAccountAge]: "Edad mínima de la cuenta",
[LangKeys.MarketFilterAccountDescMinAccountAge]:
"Mostrar solo ofertas comerciales con una edad mínima de cuenta.",
[LangKeys.MarketFilterAccountLabelAmountTrades]:
"Cantidad mínima de operaciones",
[LangKeys.MarketFilterAccountDescAmountTrades]:
"Mostrar solo ofertas comerciales de cuentas con una cantidad mínima de operaciones completadas",
[LangKeys.MarketFilterAccountTrades]: "Vientos alisios",
[LangKeys.MarketFilterAccountDays]: "Días",
[LangKeys.MarketFilterAccountClearFiltersBtn]: "Borrar filtros",
[LangKeys.MarketFilterAccountSaveBtn]: "Guardar filtros",
[LangKeys.MarketAmountFilterFieldMinAmountTrades]:
"Cantidad mínima de operaciones",
[LangKeys.MarketAmountFilterFieldMinAmountTradesDesc]:
"Establece la cantidad mínima que deseas comprar.",
[LangKeys.MarketAmountFilterFieldMaxAmount]: "Importe máximo",
[LangKeys.MarketAmountFilterFieldMaxAmountDesc]:
"Establece la cantidad máxima que deseas comprar.",
[LangKeys.MarketAmountFilterAmountClearFiltersBtn]: "Borrar filtros",
[LangKeys.MarketAmountFilterAmountSaveBtn]: "Guardar filtros",
};
export default LangPackES;

View File

@ -31,6 +31,8 @@ export enum QueryKeys {
XmrTxs = "Haveno.XmrTransactions",
MarketsOffers = "Haveno.MarketsOffers",
MyOffers = "Haveno.MyOffers",
PaymentMethods = "Haveno.PaymentMethods",
MarketsPairs = "Haveno.MarketsPairs",
// Storage
StorageAccountInfo = "Storage.AccountInfo",

View File

@ -0,0 +1,70 @@
// =============================================================================
// 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 { QueryKeys } from "@constants/query-keys";
interface MarketsPairsData {
fromPair: string;
toPair: string;
lastPrice: number;
lastPriceCurrency: string;
dayChangeRate: number;
dayChangeVolume: number;
}
export function useMarketsPairs() {
return useQuery<Array<MarketsPairsData>, Error>(
[QueryKeys.MarketsPairs],
// TODO: replace with actual implementation once haveno-ts is feature complete.
() => Promise.resolve(data)
);
}
const data = [
{
fromPair: "XMR",
toPair: "USD",
lastPrice: 246.23,
lastPriceCurrency: "EUR",
dayChangeRate: 0.2,
dayChangeVolume: 0.2,
},
{
fromPair: "XMR",
toPair: "USD",
lastPrice: 246.23,
lastPriceCurrency: "EUR",
dayChangeRate: 0.2,
dayChangeVolume: 0.2,
},
{
fromPair: "XMR",
toPair: "USD",
lastPrice: 246.23,
lastPriceCurrency: "EUR",
dayChangeRate: 0.2,
dayChangeVolume: 0.2,
},
{
fromPair: "XMR",
toPair: "USD",
lastPrice: 246.23,
lastPriceCurrency: "EUR",
dayChangeRate: 0.2,
dayChangeVolume: 0.2,
},
] as Array<MarketsPairsData>;

View File

@ -0,0 +1,69 @@
// =============================================================================
// 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 { QueryKeys } from "@constants/query-keys";
interface PaymentMethodsQuery {
assetCode?: string;
}
interface PaymentMethodsValues {
methodName: string;
methodKey: string;
rateTradeLimit: number;
rateTradeLimitCurrency: string;
info: string;
}
export function usePaymentMethods(query?: PaymentMethodsQuery) {
return useQuery<Array<PaymentMethodsValues>, Error>(
[QueryKeys.PaymentAccounts, query],
// TODO: replace with actual implementation once haveno-ts is feature complete.
() => Promise.resolve(data)
);
}
const data = [
{
methodName: "Celpay",
methodKey: "celpay",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global",
},
{
methodName: "Celpay",
methodKey: "celpay2",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global",
},
{
methodName: "Celpay",
methodKey: "celpay3",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global",
},
{
methodName: "Celpay",
methodKey: "celpay4",
rateTradeLimit: 20,
rateTradeLimitCurrency: "XMR",
info: "Global",
},
] as Array<PaymentMethodsValues>;

View File

@ -14,16 +14,20 @@
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
import { createStyles, Stack } from "@mantine/core";
import { NavbarLayout } from "@templates/NavbarLayout";
import { MarketsOffers } from "@organisms/MarketsOffers";
import { MarketOffersFilterBar } from "@organisms/MarketOffersFilterBar";
export function MarketsOffersPage() {
const { classes } = useStyles();
return (
<NavbarLayout classNames={{ contentArea: classes.contentArea }}>
<MarketsOffers />
<Stack spacing={0} className={classes.innerContent}>
<MarketOffersFilterBar />
<MarketsOffers />
</Stack>
</NavbarLayout>
);
}
@ -32,4 +36,7 @@ const useStyles = createStyles(() => ({
contentArea: {
padding: 0,
},
innerContent: {
width: "100%",
},
}));

View File

@ -0,0 +1,40 @@
// =============================================================================
// 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, Stack } from "@mantine/core";
import { NavbarLayout } from "@templates/NavbarLayout";
import { MarketOffersFilterBar } from "@organisms/MarketOffersFilterBar";
export function MarketsTransactionsPage() {
const { classes } = useStyles();
return (
<NavbarLayout classNames={{ contentArea: classes.contentArea }}>
<Stack spacing={0} className={classes.innerContentArea}>
<MarketOffersFilterBar />
</Stack>
</NavbarLayout>
);
}
const useStyles = createStyles(() => ({
contentArea: {
padding: 0,
},
innerContentArea: {
width: "100%",
},
}));

View File

@ -0,0 +1,52 @@
// =============================================================================
// 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 {
atom,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
export type OfferFilter = {
direction: string;
assetCode: string;
minimumCryptoAmount?: number | null;
maximumCryptoAmount?: number | null;
minimumBaseCurrencyAmount?: number | null;
maximumBaseCurrencyAmount?: number | null;
paymentMethods?: Array<string>;
signedAccounts?: boolean;
minimumAccountAge?: number | null;
minimumTradesAmount?: number | null;
};
const defaultValue = {
direction: "sell",
assetCode: "BTC",
};
export const offersFilterAtom = atom<OfferFilter>({
key: "offersFilter",
default: defaultValue,
});
export const useGetOffersFilterState = () =>
useRecoilValue<OfferFilter>(offersFilterAtom);
export const useSetOffersFilterState = () =>
useSetRecoilState<OfferFilter>(offersFilterAtom);
export const useOffersFilterState = () =>
useRecoilState<OfferFilter>(offersFilterAtom);

View File

@ -0,0 +1,36 @@
// =============================================================================
// 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 _ from "lodash";
/**
* Retrieves the form fields and removes fields that out of form from
* the given intial values, and removes previously unfilled optional values
* come as `null` as well.
*
* @param {Record<string, unknown>} param - Form values.
* @param {Record<string, unknown>} initialValues - Form initial values.
* @return {Record<string, unknown>}
*/
export const transformToForm = (
obj: Record<string, unknown>,
initialValues: Record<string, unknown>
): Record<string, unknown> => {
return _.pickBy(
obj,
(val, key) => val !== null && Object.keys(initialValues).includes(key)
);
};