chore: Account Settings screen

---

Reviewed-by: schowdhuri
This commit is contained in:
Ahmed Bouhuolia 2022-05-09 14:06:07 +02:00 committed by GitHub
parent d8dd987ccf
commit 7bcf36d595
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1100 additions and 20 deletions

View File

@ -14,9 +14,6 @@
// limitations under the License.
// =============================================================================
import { RecoilRoot } from "recoil";
import { HashRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { AppProviders } from "@atoms/AppProviders";
export const parameters = {
@ -29,10 +26,4 @@ export const parameters = {
},
};
export const decorators = [
(Story) => (
<HashRouter>
<AppProviders>{Story()}</AppProviders>
</HashRouter>
),
];
export const decorators = [(Story) => <AppProviders>{Story()}</AppProviders>];

View File

@ -0,0 +1 @@
<svg fill="currentColor" width="58px" height="54px" viewBox="0 0 58 54" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M43.8339 7.56984C41.5907 3.08354 36.9294 0 31.5427 0C25.2052 0 19.8717 4.2683 18.299 10.0676C17.5276 9.7233 16.672 9.53174 15.7714 9.53174C12.8205 9.53174 10.3534 11.588 9.74421 14.3362C9.47095 14.3111 9.19411 14.2983 8.91425 14.2983C3.99105 14.2983 0 18.2611 0 23.1493C0 28.0376 3.99105 32.0003 8.91425 32.0003C8.95701 32.0003 8.99969 32 9.0423 31.9994H45.1782L45.257 31.9996C52.0737 31.9996 57.5998 26.5128 57.5998 19.7444C57.5998 12.9761 52.0737 7.48926 45.257 7.48926C44.7757 7.48926 44.3008 7.51661 43.8339 7.56984Z" fill="currentColor"/> <path d="M26.2105 43.7896L22.1053 40.0001L18 43.7896" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M22.1052 52.316V40.2739" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M39.4713 48.8423L35.366 52.6318L31.2607 48.8423" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M35.366 40.3158V52.3579" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,5 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 62" width="32" height="62">
<title>server</title>
<path id="Layer" fill-rule="evenodd" fill="currentColor" d="m31.6 25.8v34.5c0 1-0.9 1.7-2 1.7h-27.6c-1.2 0-2-0.7-2-1.7v-34.5c0-1 0.8-1.7 2-1.7h27.6c1.1 0 2 0.7 2 1.7zm-6.9 8.8c0.6 0 1-0.3 1-0.8v-4.1c0-0.5-0.4-0.8-1-0.8h-17.8c-0.6 0-1 0.3-1 0.8v4.1c0 0.5 0.4 0.8 1 0.8zm1 4c0-0.5-0.4-0.8-1-0.8h-17.8c-0.5 0-0.9 0.3-0.9 0.8v4c0 0.5 0.4 0.9 0.9 0.9h17.8c0.6 0 1-0.4 1-0.9zm-11.8 10.2c0 1.1 0.8 2 1.9 2 1.1 0 2-0.9 2-2 0-1.1-0.9-1.9-2-1.9-1.1 0-1.9 0.8-1.9 1.9zm0 6.9c0 1.1 0.8 2 1.9 2 1.1 0 2-0.9 2-2 0-1.1-0.9-2-2-2-1.1 0-1.9 0.9-1.9 2z"/>
<path id="Layer" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="m5.4 5.8l4.1-3.8 4.1 3.8m-4.1 8.5v-12m17.3 8.5l-4.1 3.8-4.1-3.8m4.1-8.5v12.1"/>
</svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@ -18,7 +18,7 @@ import { Routes, Route } from "react-router-dom";
import { Home, Welcome } from "@pages/Onboarding";
import { Wallet } from "@pages/Wallet";
import { AccountPaymentAccounts } from "@pages/Account/AccountPaymentAccounts";
import { AccountNodeSettings } from "@pages/Account/AccountNodeSettings";
import { AccountNodeSettings } from "@pages/Account/NodeSettings";
import { AccountBackup } from "@pages/Account/AccountBackup";
import { AccountWallet } from "@pages/Account/AccountWallet";
import { AccountSecurity } from "@pages/Account/Security";

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 type { ComponentStory, ComponentMeta } from "@storybook/react";
import { NodeConnectSwitch } from ".";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
export default {
title: "atoms/NodeConnectSwitch",
component: NodeConnectSwitch,
} as ComponentMeta<typeof NodeConnectSwitch>;
const Template: ComponentStory<typeof NodeConnectSwitch> = () => {
return (
<NodeConnectSwitch>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
>
Remote Node
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
);
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,102 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { createStyles } from "@mantine/core";
export const useTabsStyles = createStyles<string, void>(() => {
return {
root: {},
tabsListWrapper: {
display: "flex",
},
body: {
marginTop: "2.5rem",
},
};
});
export const useControlStyles = createStyles<
string,
{ active: boolean; current: boolean }
>((theme, { active }, getRef) => {
const tabActive = { ref: getRef("tabActive") };
return {
tabControl: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.white,
color:
theme.colorScheme === "dark"
? theme.colors.dark[0]
: theme.colors.gray[9],
border: `2px solid ${
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[2]
}`,
fontSize: theme.fontSizes.md,
padding: `${theme.spacing.lg}px ${theme.spacing.xl}px`,
borderRadius: theme.radius.lg,
height: "8.45rem",
width: "13.85rem",
"&:not(:first-of-type)": {
marginLeft: theme.spacing.xl,
},
[`&.${tabActive.ref}`]: {
color: theme.colorScheme === "dark" ? theme.black : theme.white,
},
cursor: "pointer",
position: "relative",
},
tabIcon: {
fill: "currentColor",
minHeight: "3.8rem",
display: "flex",
svg: {
margin: "auto",
},
},
tabInner: {
flexDirection: "column",
},
tabLabel: {
fontWeight: 600,
fontSize: theme.fontSizes.md,
marginTop: "1rem",
},
tabActive: {
backgroundColor: theme.colors.blue[6],
borderColor: theme.colors.blue[6],
color: theme.white,
},
tabCurrent: {
display: "inline-block",
position: "absolute",
fontSize: theme.fontSizes.xs,
lineHeight: 1,
padding: "0.38rem 1.15rem",
borderRadius: theme.radius.sm,
top: "0.6rem",
left: "0.7rem",
background: active
? theme.fn.rgba(theme.white, 0.15)
: theme.fn.rgba(theme.colors.blue[5], 0.15),
color: active ? theme.white : theme.black,
},
};
});

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 { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeConnectSwitch } from ".";
describe("atoms::NodeConnectSwitch", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<NodeConnectSwitch>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
active={false}
current={false}
>
Remote Node
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
});

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 type { ReactElement, ReactNode } from "react";
import { cloneElement, Children } from "react";
import { Box } from "@mantine/core";
import { useUncontrolled } from "@mantine/hooks";
import { NodeConnectSwitchMethod } from "./NodeConnectSwitchMethod";
import { useTabsStyles } from "./NodeConnectSwitch.style";
interface NodeConnectSwitchProps {
/** <Tab /> components only */
children?: ReactNode;
/** Key of active tab */
active?: string;
/** Key of current tab */
current?: boolean;
/** Called when tab control is clicked with tab index */
onTabChange?(tabKey: string): void;
/** Key of initial tab */
initialTab?: string;
/** Extra class name */
className?: string;
}
export function NodeConnectSwitch({
className,
onTabChange,
active,
children,
initialTab,
}: NodeConnectSwitchProps) {
const { classes, cx } = useTabsStyles();
const tabs = Children.toArray(children) as Array<ReactElement>;
const [_activeTab, handleActiveTabChange] = useUncontrolled({
value: active,
defaultValue: initialTab,
finalValue: "",
rule: (value) => typeof value === "string",
onChange: (tabKey: string) => {
onTabChange && onTabChange(tabKey);
},
});
const panes = tabs.map((tab, index) =>
cloneElement(tab, {
key: index,
active: _activeTab === tab.props.tabKey,
onClick: () => handleActiveTabChange(tab.props.tabKey),
})
);
const content = tabs.find((tab) => tab.props.tabKey === _activeTab);
return (
<Box className={cx(classes.root, className)}>
<div className={cx(classes.tabsListWrapper)}>{panes} </div>
{content && (
<div role="tabpanel" className={classes.body} key={_activeTab}>
{content.props.children}
</div>
)}
</Box>
);
}
NodeConnectSwitch.Method = NodeConnectSwitchMethod;

View File

@ -0,0 +1,74 @@
// =============================================================================
// 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 { LangKeys } from "@constants/lang";
import { Box } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { useControlStyles } from "./NodeConnectSwitch.style";
interface SettingTabProps {
active?: boolean;
current?: boolean;
className?: string;
icon?: React.ReactNode;
label: string | React.ReactNode;
tabKey: string;
children: React.ReactNode;
}
export function NodeConnectSwitchMethod({
active,
current,
label,
icon,
className,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
tabKey,
...rest
}: SettingTabProps) {
const { classes, cx } = useControlStyles({
active: active || false,
current: current || false,
});
return (
<Box
component="button"
tabIndex={active ? 0 : -1}
className={cx(
classes.tabControl,
{ [classes.tabActive]: active },
className
)}
type="button"
role="tab"
{...rest}
>
<Box className={classes.tabInner}>
{current && (
<Box className={cx(classes.tabCurrent)}>
<FormattedMessage
id={LangKeys.AccountSettingsCurrent}
defaultMessage={"Current"}
/>
</Box>
)}
{icon && <Box className={classes.tabIcon}>{icon}</Box>}
{label && <Box className={classes.tabLabel}>{label}</Box>}
</Box>
</Box>
);
}

View File

@ -0,0 +1,121 @@
// Vitest Snapshot v1
exports[`atoms::NodeConnectSwitch > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-15po0m8"
>
<div
class="mantine-17do188"
>
<button
class="mantine-1lhe3fe"
role="tab"
tabindex="-1"
type="button"
>
<div
class="mantine-199rwtt"
>
<div
class="mantine-1jn9p7a"
>
Current
</div>
<div
class="mantine-9bd5vi"
>
<svg
height="62px"
viewBox="0 0 32 62"
width="32px"
xmlns="http://www.w3.org/2000/svg"
>
<title>
server
</title>
<path
d="m31.6 25.8v34.5c0 1-0.9 1.7-2 1.7h-27.6c-1.2 0-2-0.7-2-1.7v-34.5c0-1 0.8-1.7 2-1.7h27.6c1.1 0 2 0.7 2 1.7zm-6.9 8.8c0.6 0 1-0.3 1-0.8v-4.1c0-0.5-0.4-0.8-1-0.8h-17.8c-0.6 0-1 0.3-1 0.8v4.1c0 0.5 0.4 0.8 1 0.8zm1 4c0-0.5-0.4-0.8-1-0.8h-17.8c-0.5 0-0.9 0.3-0.9 0.8v4c0 0.5 0.4 0.9 0.9 0.9h17.8c0.6 0 1-0.4 1-0.9zm-11.8 10.2c0 1.1 0.8 2 1.9 2 1.1 0 2-0.9 2-2 0-1.1-0.9-1.9-2-1.9-1.1 0-1.9 0.8-1.9 1.9zm0 6.9c0 1.1 0.8 2 1.9 2 1.1 0 2-0.9 2-2 0-1.1-0.9-2-2-2-1.1 0-1.9 0.9-1.9 2z"
fill="currentColor"
fill-rule="evenodd"
id="Layer"
/>
<path
d="m5.4 5.8l4.1-3.8 4.1 3.8m-4.1 8.5v-12m17.3 8.5l-4.1 3.8-4.1-3.8m4.1-8.5v12.1"
id="Layer"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
/>
</svg>
</div>
<div
class="mantine-1wlt8yk"
>
Local Node
</div>
</div>
</button>
<button
class="mantine-1lhe3fe"
role="tab"
tabindex="-1"
type="button"
>
<div
class="mantine-199rwtt"
>
<div
class="mantine-9bd5vi"
>
<svg
fill="none"
height="54px"
viewBox="0 0 58 54"
width="58px"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M43.8339 7.56984C41.5907 3.08354 36.9294 0 31.5427 0C25.2052 0 19.8717 4.2683 18.299 10.0676C17.5276 9.7233 16.672 9.53174 15.7714 9.53174C12.8205 9.53174 10.3534 11.588 9.74421 14.3362C9.47095 14.3111 9.19411 14.2983 8.91425 14.2983C3.99105 14.2983 0 18.2611 0 23.1493C0 28.0376 3.99105 32.0003 8.91425 32.0003C8.95701 32.0003 8.99969 32 9.0423 31.9994H45.1782L45.257 31.9996C52.0737 31.9996 57.5998 26.5128 57.5998 19.7444C57.5998 12.9761 52.0737 7.48926 45.257 7.48926C44.7757 7.48926 44.3008 7.51661 43.8339 7.56984Z"
fill="currentColor"
fill-rule="evenodd"
/>
<path
d="M26.2105 43.7896L22.1053 40.0001L18 43.7896"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
/>
<path
d="M22.1052 52.316V40.2739"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
/>
<path
d="M39.4713 48.8423L35.366 52.6318L31.2607 48.8423"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
/>
<path
d="M35.366 40.3158V52.3579"
stroke="currentColor"
stroke-linecap="round"
stroke-width="2"
/>
</svg>
</div>
<div
class="mantine-1wlt8yk"
>
Remote Node
</div>
</div>
</button>
</div>
</div>
</DocumentFragment>
`;

View File

@ -14,12 +14,4 @@
// limitations under the License.
// =============================================================================
import { AccountLayout } from "@templates/AccountLayout";
export function AccountNodeSettings() {
return (
<AccountLayout>
<h1>Account Node Settings</h1>
</AccountLayout>
);
}
export * from "./NodeConnectSwitch";

View File

@ -0,0 +1,46 @@
// =============================================================================
// 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 { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { NodeStatus, NodeStatusType } from ".";
export default {
title: "atoms/NodeStatus",
component: NodeStatus,
} as ComponentMeta<typeof NodeStatus>;
const Template: ComponentStory<typeof NodeStatus> = () => {
return (
<Stack>
<NodeStatus
title={"node.moneroworldcom:18089"}
status={NodeStatusType.Active}
/>
<NodeStatus
title={"node.xmr.pt:18081"}
status={NodeStatusType.Inactive}
/>
<NodeStatus
title={"node.monero.net:18081"}
status={NodeStatusType.Active}
/>
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {};

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 } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { NodeStatus, NodeStatusType } from "./NodeStatus";
describe("atoms::NodeStatus", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<NodeStatus
title={"node.moneroworldcom:18089:active"}
status={NodeStatusType.Active}
/>
<NodeStatus
title={"node.moneroworldcom:18089:inactive"}
status={NodeStatusType.Inactive}
/>
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,77 @@
// =============================================================================
// 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, createStyles, Text } from "@mantine/core";
export enum NodeStatusType {
Active = "active",
Inactive = "inactive",
}
export interface NodeStatusProps {
/** Node title */
title: string;
/** Node status */
status: NodeStatusType;
}
export function NodeStatus({ title, status }: NodeStatusProps) {
const { classes } = useStyles({ status });
return (
<Box className={classes.root}>
<Text className={classes.title}>{title}</Text>
<div className={classes.status}>
<div className={classes.statusInner} />
</div>
</Box>
);
}
export const useStyles = createStyles<string, { status: NodeStatusType }>(
(theme, { status }) => {
return {
root: {
backgroundColor:
theme.colorScheme === "dark" ? theme.colors.dark[8] : theme.white,
border: `1px solid ${theme.colors.gray[2]}`,
borderRadius: theme.radius.md,
padding: "0.91rem",
display: "flex",
transition: "background-color 0.1s ease-in-out",
},
title: {
fontWeight: 600,
fontSize: theme.fontSizes.sm,
lineHeight: 1,
width: "100%",
},
status: {
display: "flex",
},
statusInner: {
height: "0.625rem",
width: "0.625rem",
borderRadius: "0.625rem",
background:
status === NodeStatusType.Active
? theme.colors.green[4]
: theme.colors.gray[4],
margin: "auto",
},
};
}
);

View File

@ -0,0 +1,38 @@
// Vitest Snapshot v1
exports[`atoms::NodeStatus > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-18xx7au"
>
<div
class="mantine-Text-root mantine-14byb36"
>
node.moneroworldcom:18089:active
</div>
<div
class="mantine-17do188"
>
<div
class="mantine-167633s"
/>
</div>
</div>
<div
class="mantine-18xx7au"
>
<div
class="mantine-Text-root mantine-14byb36"
>
node.moneroworldcom:18089:inactive
</div>
<div
class="mantine-17do188"
>
<div
class="mantine-1cipvbv"
/>
</div>
</div>
</DocumentFragment>
`;

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

View File

@ -29,6 +29,17 @@ export enum LangKeys {
AccountSidebarNodeSettings = "account.sidebar.nodeSettings",
AccountSecurityTitle = "account.security.title",
AccountSecurityDesc = "account.security.desc",
AccountNodeSettingsTitle = "account.nodeSecurity.title",
AccountNodeSettingsDesc = "account.nodeSecurity.desc",
AccountNodeSettingsLocal = "account.nodeSecurity.local.title",
AccountNodeSettingsRemote = "account.nodeSecurity.remote.title",
AccountNodeFieldBlockchainLocation = "account.nodeSecurity.blockchainLocation",
AccountNodeFieldDeamonAddress = "account.nodeSecurity.deamonAddress",
AccountNodeFieldDeamonFlags = "account.nodeSecurity.deamonFlags",
AccountNodeFieldPort = "account.nodeSecurity.port",
AccountNodeStopDeamon = "account.nodeSecurity.stopDeamon",
AccountSettingsAddNode = "account.settings.addNewNode",
AccountSettingsCurrent = "account.settings.current",
AccountSecurityFieldPassword = "account.security.field.password",
AccountSecurityFieldRepeatPassword = "account.security.field.repeatPassword",
AccountSecurityFieldCurrentPassword = "account.security.field.currentPassword",

View File

@ -33,6 +33,18 @@ const LangPackEN: { [key in LangKeys]: string } = {
[LangKeys.AccountSecurityTitle]: "Account Security",
[LangKeys.AccountSecurityDesc]:
"Haveno does not store any of your data, this happens solely locally on your device. Its not possible to restore your password when lost. Please make sure you store a copy of it on a safe place.",
[LangKeys.AccountNodeSettingsDesc]:
"Using a local node is recommended, but does require loading the entire blockchain. Choose remote node if you prefer a faster but less secure experience.",
[LangKeys.AccountNodeSettingsTitle]: "Your node settings",
[LangKeys.AccountNodeSettingsLocal]: "Local Node",
[LangKeys.AccountNodeSettingsRemote]: "Remote Node",
[LangKeys.AccountNodeFieldBlockchainLocation]: "Blockchain location",
[LangKeys.AccountNodeFieldDeamonAddress]: "Deamon Address",
[LangKeys.AccountNodeFieldPort]: "Port",
[LangKeys.AccountNodeFieldDeamonFlags]: "Deamon startup flags",
[LangKeys.AccountNodeStopDeamon]: "Stop deamon",
[LangKeys.AccountSettingsAddNode]: "Add a new node",
[LangKeys.AccountSettingsCurrent]: "Current",
[LangKeys.AccountSecurityFieldPassword]: "Password",
[LangKeys.AccountSecurityFieldRepeatPassword]: "Repeat new password",
[LangKeys.AccountSecurityFieldCurrentPassword]: "Current password",

View File

@ -33,6 +33,19 @@ const LangPackES: { [key in LangKeys]: string } = {
[LangKeys.AccountSecurityTitle]: "Seguridad de la cuenta",
[LangKeys.AccountSecurityDesc]:
"Haveno no almacena ninguno de sus datos, esto ocurre únicamente localmente en su dispositivo. No es posible restaurar su contraseña cuando se pierde. Asegúrese de guardar una copia en un lugar seguro.",
[LangKeys.AccountNodeSettingsDesc]:
"Se recomienda usar un nodo local, pero requiere cargar toda la cadena de bloques. Elija 'nodo remoto' si prefiere una experiencia más rápida pero menos segura.",
[LangKeys.AccountNodeSettingsTitle]: "La configuración de tu nodo",
[LangKeys.AccountNodeSettingsLocal]: "Nodo Local",
[LangKeys.AccountNodeSettingsRemote]: "Nodo Remoto",
[LangKeys.AccountNodeFieldBlockchainLocation]:
"Ubicación de cadena de bloques",
[LangKeys.AccountNodeFieldDeamonAddress]: "Dirección del demonio",
[LangKeys.AccountNodeFieldPort]: "Puerto",
[LangKeys.AccountNodeFieldDeamonFlags]: "Indicadores de inicio de daemon",
[LangKeys.AccountNodeStopDeamon]: "Detener demonio",
[LangKeys.AccountSettingsAddNode]: "Agregar un nuevo nodo",
[LangKeys.AccountSettingsCurrent]: "Actual",
[LangKeys.AccountSecurityFieldPassword]: "Clave",
[LangKeys.AccountSecurityFieldRepeatPassword]: "Repita la nueva contraseña",
[LangKeys.AccountSecurityFieldCurrentPassword]: "Contraseña actual",

View File

@ -0,0 +1,111 @@
// =============================================================================
// 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, Stack, Grid, createStyles } from "@mantine/core";
import { useForm } from "@mantine/form";
import { Button } from "@atoms/Buttons";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { TextInput } from "@atoms/TextInput";
export function NodeLocalForm() {
const form = useForm({
initialValues: {
blockchainLocation: "",
startupFlags: "",
deamonAddress: "",
port: "",
},
});
return (
<Box>
<NodeLocalStopDeamon />
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<Stack spacing={"lg"}>
<TextInput
id={"blockchainLocation"}
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldBlockchainLocation}
defaultMessage={"Blockchain location"}
/>
}
{...form.getInputProps("blockchainLocation")}
/>
<TextInput
id={"deamonFlags"}
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDeamonFlags}
defaultMessage={"Deamon startup flags"}
/>
}
{...form.getInputProps("startupFlags")}
/>
<Grid>
<Grid.Col span={9}>
<TextInput
id={"deamonAddress"}
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldDeamonAddress}
defaultMessage={"Deamon Address"}
/>
}
{...form.getInputProps("deamonAddress")}
/>
</Grid.Col>
<Grid.Col span={3}>
<TextInput
id={"port"}
label={
<FormattedMessage
id={LangKeys.AccountNodeFieldPort}
defaultMessage={"Port"}
/>
}
{...form.getInputProps("port")}
/>
</Grid.Col>
</Grid>
</Stack>
</form>
</Box>
);
}
function NodeLocalStopDeamon() {
const { classes } = useStyles();
return (
<div className={classes.actions}>
<Button flavor={"neutral"}>
<FormattedMessage
id={LangKeys.AccountNodeStopDeamon}
defaultMessage={"Stop deamon"}
/>
</Button>
</div>
);
}
const useStyles = createStyles((theme) => ({
actions: {
marginBottom: theme.spacing.xl,
},
}));

View File

@ -0,0 +1,79 @@
// =============================================================================
// 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 { Stack, createStyles, Group } from "@mantine/core";
import { Button } from "@atoms/Buttons";
import { NodeStatus, NodeStatusType } from "@atoms/NodeStatus";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
export function NodeRemoteStatus() {
return (
<Stack>
<NodeStatus
title={"node.moneroworldcom:18089"}
status={NodeStatusType.Active}
/>
<NodeStatus
title={"node.xmr.pt:18081"}
status={NodeStatusType.Inactive}
/>
<NodeStatus
title={"node.monero.net:18081"}
status={NodeStatusType.Active}
/>
<AddNewNodeButton />
<Group position={"right"} mt={"sm"}>
<Button size={"md"}>
<FormattedMessage id={LangKeys.Save} defaultMessage={"Save"} />
</Button>
</Group>
</Stack>
);
}
function AddNewNodeButton({ ...rest }) {
const { classes } = useStyles();
return (
<Button
variant={"subtle"}
color={"dark"}
classNames={{
root: classes.root,
inner: classes.inner,
}}
{...rest}
>
+{" "}
<FormattedMessage
id={LangKeys.AccountSettingsAddNode}
defaultMessage={"Add a new node"}
/>
</Button>
);
}
const useStyles = createStyles(() => ({
root: {
padding: "0.8rem",
height: 45,
},
inner: {
justifyContent: "start",
},
}));

View File

@ -0,0 +1,57 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack, Box, createStyles } from "@mantine/core";
import { AccountLayout } from "@templates/AccountLayout";
import { LangKeys } from "@constants/lang";
import { NodeSettingsSwitch } from "./NodeSettingsSwitch";
import { WIDTH } from "./_constants";
import { BodyText, Heading } from "@atoms/Typography";
export function AccountNodeSettings() {
const { classes } = useStyles();
return (
<AccountLayout>
<Box className={classes.content}>
<Stack spacing={"sm"}>
<Heading stringId={LangKeys.AccountNodeSettingsTitle} order={3}>
Your node settings
</Heading>
<BodyText
stringId={LangKeys.AccountNodeSettingsDesc}
size={"md"}
className={classes.paragraph}
>
Using a local node is recommended, but does require loading the
entire blockchain. Choose remote node if you prefer a faster but
less secure experience.
</BodyText>
<NodeSettingsSwitch />
</Stack>
</Box>
</AccountLayout>
);
}
const useStyles = createStyles((theme) => ({
content: {
maxWidth: WIDTH,
},
paragraph: {
marginBottom: theme.spacing.xl,
},
}));

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 { createStyles } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { NodeConnectSwitch } from "@atoms/NodeConnectSwitch";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeLocalForm } from "./NodeLocalForm";
import { NodeRemoteStatus } from "./NodeRemoteStatus";
export function NodeSettingsSwitch() {
const { classes } = useStyles();
return (
<NodeConnectSwitch
initialTab={"local-node"}
className={classes.connectSwitch}
>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsLocal}
defaultMessage={"Local Node"}
/>
}
icon={<ServerIcon width={32} height={62} />}
>
<NodeLocalForm />
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
label={
<FormattedMessage
id={LangKeys.AccountNodeSettingsRemote}
defaultMessage={"Remote Node"}
/>
}
icon={<CloudIcon width={58} height={54} />}
>
<NodeRemoteStatus />
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
);
}
const useStyles = createStyles(() => ({
connectSwitch: {
marginBottom: "2rem",
},
}));

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 const WIDTH = 470;

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