Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PE-6149: delegated staking landing page and stake/unstake modal #8

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
"\\.(css|gif|png)$": "jest-transform-stub",
"\\.svg": "<rootDir>/tests/common/mocks/svg.js",
"^@tests/(.*)$": "<rootDir>/tests/$1",
"^@src/(.*)$": "<rootDir>/src/$1"
"^@src/(.*)$": "<rootDir>/src/$1",
"@ar.io/sdk/web": "<rootDir>/node_modules/@ar.io/sdk/lib/cjs/web/index.js",
"warp-contracts": "<rootDir>/node_modules/warp-contracts/lib/cjs/index.js"
},
"transform": {
"^.+\\.(ts|tsx|js|jsx|mjs)$": ["ts-jest", { "useESM": true }]
},
"transformIgnorePatterns": [
"/node_modules/(?!arbundles|arweave-wallet-connector).+\\.js$"
],
"moduleDirectories": ["node_modules", "assets"],
"testPathIgnorePatterns": ["tests/common/"],
"coverageThreshold": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"vis": "yarn vite-bundle-visualizer"
},
"dependencies": {
"@ar.io/sdk": "^1.0.7-alpha.2",
"@ar.io/sdk": "^1.0.7",
"@fontsource/rubik": "^5.0.19",
"@headlessui/react": "^1.7.19",
"@radix-ui/react-tooltip": "^1.0.7",
Expand Down
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import PendingInteractionsProvider from './components/PendingInteractionsProvide
// const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Gateways = React.lazy(() => import('./pages/Gateways'));
const Gateway = React.lazy(() => import('./pages/Gateway'));
// const Staking = React.lazy(() => import('./pages/Staking'));
const Staking = React.lazy(() => import('./pages/Staking'));
// const Observers = React.lazy(() => import('./pages/Observers'));

const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createBrowserRouter);
Expand Down Expand Up @@ -58,7 +58,7 @@ function App() {
}
/>
,
{/* <Route
<Route
path="staking"
element={
<Suspense fallback={<Loading />}>
Expand All @@ -67,7 +67,7 @@ function App() {
}
/>
,
<Route
{/*<Route
path="observers"
element={
<Suspense fallback={<Loading />}>
Expand Down
7 changes: 6 additions & 1 deletion src/components/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ const Profile = () => {
text="Connect"
onClick={() => setIsModalOpen(true)}
/>
<ConnectModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
{isModalOpen && (
Copy link
Contributor

@atticusofsparta atticusofsparta May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these params seem redundant no? Atleast with usage - "open" should handle the rendering internally right? If not, then "open" could just always be set to true and you just use this pattern to render it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with one style of using the param but then switched to using it with the conditional rendering so that it would get fully unmounted from the DOM when not used. I'll take on refactoring out the open param and require using the && pattern in the next PR to keep this PR moving forward.

<ConnectModal
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
)}
</div>
) : (
<div></div>
Expand Down
33 changes: 33 additions & 0 deletions src/components/forms/ErrorMessageIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { FormErrorIcon } from '../icons';

const ErrorMessageIcon = ({
errorMessage,
tooltipPadding = 0,
}: {
errorMessage: string;
tooltipPadding?: number;
}) => {

const marginBottom = tooltipPadding ? `mb-[${tooltipPadding}px]` : '';

return (
<div className="relative flex px-[12px] text-red-600">
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<FormErrorIcon />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-50 w-fit max-w-[400px] rounded-md bg-red-1000 px-[24px] py-[12px]">
<Tooltip.Arrow className={`fill-red-1000 ${marginBottom}`} />
<div className="text-sm text-red-600">{errorMessage}</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
</div>
);
};

export default ErrorMessageIcon;
53 changes: 18 additions & 35 deletions src/components/forms/FormRow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { FormErrorIcon, ResetIcon } from '../icons';
import { ResetIcon } from '../icons';
import ErrorMessageIcon from './ErrorMessageIcon';
import FormSwitch from './FormSwitch';

export enum RowType {
TOP,
MIDDLE,
BOTTOM,
SINGLE,
LAST // special hack for last row due to rounding of container vs form
LAST, // special hack for last row due to rounding of container vs form
}

const ROUND_STYLES = {
Expand Down Expand Up @@ -71,7 +71,7 @@ const FormRow = ({
const cleared = { ...errorMessages };
delete cleared[formPropertyName];
setErrorMessages(cleared);
}
};

const resetFormValue = () => {
if (initialState) {
Expand All @@ -83,7 +83,6 @@ const FormRow = ({
}
};


return (
<>
<div className="bg-grey-900 pb-px">
Expand Down Expand Up @@ -113,11 +112,11 @@ const FormRow = ({
title={`${value ? 'Disable' : 'Enable'} ${label}`}
/>

{modified &&

// using fixed position to avoid the modified dot from being clipped by the overflow-hidden parent
// may need to revisit if form parent placement changes to not be flush right with viewport
<ModifiedDot className="fixed right-[17.5px] z-10" />}
{modified && (
// using fixed position to avoid the modified dot from being clipped by the overflow-hidden parent
// may need to revisit if form parent placement changes to not be flush right with viewport
<ModifiedDot className="fixed right-[17.5px] z-10" />
)}
</div>
) : (
<div
Expand Down Expand Up @@ -164,34 +163,18 @@ const FormRow = ({
}
}}
/>
{hasError && (
<div className="relative flex px-[12px] text-red-600">
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<FormErrorIcon />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-50 w-fit rounded-md bg-red-1000 px-[24px] py-[12px]">
<Tooltip.Arrow className="fill-red-1000" />
<div className="text-sm text-red-600">
{errorMessage}
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
</div>
)}
{enabled && initialState && initialState[formPropertyName] !== value && (
<button className="pr-[16px]" onClick={resetFormValue}>
<ResetIcon />
</button>
)}
{hasError && <ErrorMessageIcon errorMessage={errorMessage} />}
{enabled &&
initialState &&
initialState[formPropertyName] !== value && (
<button className="pr-[16px]" onClick={resetFormValue}>
<ResetIcon />
</button>
)}
{rightComponent}
{enabled && modified && (
// using fixed position to avoid the modified dot from being clipped by the overflow-hidden parent
// may need to revisit if form parent placement changes to not be flush right with viewport
// may need to revisit if form parent placement changes to not be flush right with viewport
<ModifiedDot className="fixed right-[17.5px] z-10" />
)}
</div>
Expand Down
53 changes: 47 additions & 6 deletions src/components/forms/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,18 @@ export const validateIOAmount = (
return (v: string) => {
const value = parseFloat(v);

if(max) {
return value < min || value > max || isNaN(v as unknown as number)
if (max) {
if (isNaN(v as unknown as number) || isNaN(value)) {
return `${propertyName} must be a number.`;
} else if (max <= min && value < min) {
return `${propertyName} must be a number >= ${min} IO.`;
}

return value < min || value > max
? `${propertyName} must be a number from ${min} to ${max} IO.`
: undefined;
}
return value < min || isNaN(v as unknown as number)
return value < min || isNaN(v as unknown as number) || isNaN(value)
? `${propertyName} must be a number >= ${min} IO.`
: undefined;
};
Expand All @@ -73,10 +79,45 @@ export const validateNumberRange = (
return (v: string) => {
const value = parseFloat(v);

// because parseFloat parses initial valid numbers then discards any remaining invalid text,
// need to use isNan(v as unknown as number) to check for invalid text like "3adsfwe".
return value < min || value > max || isNaN(v as unknown as number)
// because parseFloat parses initial valid numbers then discards any remaining invalid text,
// need to use isNan(v as unknown as number) to check for invalid text like "3adsfwe".
return value < min ||
value > max ||
isNaN(v as unknown as number) ||
isNaN(value)
? `${propertyName} must be a number from ${min} to ${max}.`
: undefined;
};
};

export const validateUnstakeAmount = (
propertyName: string,
currentStake: number,
minDelegatedStake: number,
): FormValidationFunction => {
return (v: string) => {
const value = parseFloat(v);

if (isNaN(v as unknown as number) || isNaN(value)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isNaN(v as unknown as number) || isNaN(value)) {
if (isNaN(+v) || isNaN(value)) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out, I updated to coerce using unary plus in this file and added a unit test.

return `${propertyName} must be a number.`;
}

if (value < 1) {
return `${propertyName} must be at least 1 IO.`;
}

if (value > currentStake) {
return `${propertyName} cannot be greater than your current stake of ${currentStake} IO.`;
}

if (
currentStake - value < minDelegatedStake &&
value != minDelegatedStake &&
value != currentStake
) {
return `Withdrawing this amount will put you below the gateway's minimum stake of ${minDelegatedStake} IO. You can either: withdraw a smaller amount so your remaining stake is above the minimum - or - withdraw your full delegated stake.`;
}

return undefined;
};
};
2 changes: 2 additions & 0 deletions src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import OpenDrawerIcon from './open_drawer.svg?react';
import PinkArrowIcon from './pink_arrow.svg?react';
import ResetIcon from './reset.svg?react';
import StakingIcon from './staking.svg?react';
import StakingSplash from './staking_splash.svg?react';
import StartGatewayCubes from './start_gateway_cubes.svg?react';
import StatsArrowIcon from './stats_arrow.svg?react';
import SuccessCheck from './success_check.svg?react';
Expand Down Expand Up @@ -52,6 +53,7 @@ export {
PinkArrowIcon,
ResetIcon,
StakingIcon,
StakingSplash,
StartGatewayCubes,
StatsArrowIcon,
SuccessCheck,
Expand Down
65 changes: 65 additions & 0 deletions src/components/icons/staking_splash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/components/modals/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ const BaseModal = ({
onClose,
children,
showCloseButton = true,
useDefaultPadding = true,
}: {
open: boolean;
onClose: () => void;
children: ReactElement;
showCloseButton?: boolean;
useDefaultPadding?: boolean;
}) => {
return (
<Dialog open={open} onClose={() => {}} className="relative z-10">
Expand All @@ -21,7 +23,9 @@ const BaseModal = ({
/>

<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<Dialog.Panel className="relative items-stretch rounded-[12px] bg-[#111112] p-[32px] text-center text-grey-100">
<Dialog.Panel
className={`relative items-stretch rounded-[12px] bg-[#111112] ${useDefaultPadding ? 'p-[32px]' : ''} text-center text-grey-100`}
>
{showCloseButton && (
<button className="absolute right-[-28px] top-0" onClick={onClose}>
<CloseIcon />
Expand Down
Loading
Loading