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 all 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
2 changes: 1 addition & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
matrix:
step: [
'lint:check',
#'test',
'test',
'build',
]
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
matrix:
step: [
'lint:check',
#'test',
'test',
'build',
]
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
matrix:
step: [
'lint:check',
#'test',
'test',
'build',
]
steps:
Expand Down
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
52 changes: 44 additions & 8 deletions src/components/forms/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,20 @@ export const validateIOAmount = (
max?: number,
): FormValidationFunction => {
return (v: string) => {
const value = parseFloat(v);
const value = +v;

if(max) {
return value < min || value > max || isNaN(v as unknown as number)
if (max) {
if (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(value)
? `${propertyName} must be a number >= ${min} IO.`
: undefined;
};
Expand All @@ -71,12 +77,42 @@ export const validateNumberRange = (
max: number,
): FormValidationFunction => {
return (v: string) => {
const value = parseFloat(v);
const value = +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)
return value < min || value > max || 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 = +v;

if (isNaN(value) || v.length === 0) {
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.
Loading
Loading