From 9be84ed5af0be7d7e31e1ba0e28e6c7f5e5b2c0b Mon Sep 17 00:00:00 2001 From: Ignacio Date: Tue, 5 Mar 2024 18:54:38 +0800 Subject: [PATCH] feat: continue with delegation details --- package-lock.json | 98 +++++++ package.json | 1 + src/features/core/components/base.tsx | 65 ++++- .../staking/components/delegation-details.tsx | 261 +++++++++--------- src/features/staking/components/main-page.tsx | 21 +- .../components/validator-delegation.tsx | 16 +- .../staking/components/validators-table.tsx | 4 +- src/features/staking/lib/core/icons.ts | 6 + src/features/staking/lib/formatters.ts | 17 ++ 9 files changed, 351 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index 909bd14..43d4703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/proto-signing": "^0.32.2", "@cosmjs/stargate": "^0.32.2", + "@mui/base": "^5.0.0-beta.37", "bignumber.js": "^9.1.2", "cosmjs-types": "^0.9.0", "next": "14.1.0", @@ -1121,6 +1122,18 @@ "@floating-ui/utils": "^0.2.0" } }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/utils": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", @@ -1500,6 +1513,82 @@ "tslib": "^2.3.1" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.37", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.37.tgz", + "integrity": "sha512-/o3anbb+DeCng8jNsd3704XtmmLDZju1Fo8R2o7ugrVtPQ/QpcqddwKNzKPZwa0J5T8YNW3ZVuHyQgbTnQLisQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.11", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.11.tgz", + "integrity": "sha512-D6bwqprUa9Stf8ft0dcMqWyWDKEo7D+6pB1k8WajbqlYIRA8J8Kw9Ra7PSZKKePGBGWO+/xxrX1U8HpG/aXQCw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/@next/env": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", @@ -1990,6 +2079,15 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@protobuf-ts/runtime": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.9.3.tgz", diff --git a/package.json b/package.json index 6b1f436..06eee68 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/proto-signing": "^0.32.2", "@cosmjs/stargate": "^0.32.2", + "@mui/base": "^5.0.0-beta.37", "bignumber.js": "^9.1.2", "cosmjs-types": "^0.9.0", "next": "14.1.0", diff --git a/src/features/core/components/base.tsx b/src/features/core/components/base.tsx index b4f47aa..a4b631f 100644 --- a/src/features/core/components/base.tsx +++ b/src/features/core/components/base.tsx @@ -1,5 +1,8 @@ +import { ClickAwayListener } from "@mui/base/ClickAwayListener"; +import { Unstable_Popup as BasePopup } from "@mui/base/Unstable_Popup"; import Link from "next/link"; -import type { PropsWithChildren } from "react"; +import type { FC, PropsWithChildren, ReactNode } from "react"; +import { useState } from "react"; import { toast } from "react-toastify"; import { clipboard } from "@/features/staking/lib/core/icons"; @@ -141,3 +144,63 @@ export const Button = ({ className, variant, ...props }: ButtonProps) => { /> ); }; + +type FloatingDropdownProps = { + children: ReactNode; + className?: string; + id: string; + isOpen: boolean; + modalClass?: string; + offset?: number; + placement?: "bottom-end" | "bottom-start" | "bottom" | "right" | "top"; + setIsOpen: (isOpen: boolean) => void; + Trigger: FC<{ id?: string }>; +}; + +// @TODO: Implement for settings +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const FloatingDropdown = ({ + children, + className, + id, + isOpen, + modalClass, + offset, + placement, + setIsOpen, + Trigger, +}: FloatingDropdownProps) => { + const [anchor, setAnchor] = useState(null); + + return ( +
+ + { + if (isOpen) { + setIsOpen?.(false); + } + }} + > + + {children} + + +
+ ); +}; diff --git a/src/features/staking/components/delegation-details.tsx b/src/features/staking/components/delegation-details.tsx index 8c1d709..9ead94a 100644 --- a/src/features/staking/components/delegation-details.tsx +++ b/src/features/staking/components/delegation-details.tsx @@ -9,7 +9,12 @@ import { getTotalDelegation, getTotalUnbonding } from "../context/selectors"; import type { StakingContextType, StakingState } from "../context/state"; import { useValidatorLogo } from "../hooks"; import { coinIsPositive } from "../lib/core/coins"; -import { formatCoin, formatCommission } from "../lib/formatters"; +import { pointer } from "../lib/core/icons"; +import { + formatCoin, + formatCommission, + formatUnbondingCompletionTime, +} from "../lib/formatters"; import AddressShort from "./address-short"; import TokenColors from "./token-colors"; @@ -22,11 +27,42 @@ export const getCanShowDetails = (state: StakingState) => { ); }; +type Props = { + isShowingDetails: boolean; + setIsShowingDetails: (isShowingDetails: boolean) => void; +}; + +export const DetailsTrigger = ({ + isShowingDetails, + setIsShowingDetails, +}: Props) => ( + +); + const gridStyle = { gap: "16px", gridTemplateColumns: "60px 1.5fr repeat(4, 1fr)", }; +const rowStyle = + "grid w-full items-center justify-between gap-2 p-2 mb-[24px] last:mb-[0px] bg-black rounded-[8px]"; + +const wrapperStyle = + "w-full overflow-hidden rounded-[24px] bg-bg-600 pb-4 text-typo-100 px-[16px]"; + type DelegationRowProps = { delegation: NonNullable["items"][number]; disabled?: boolean; @@ -45,75 +81,66 @@ const DelegationRow = ({ const logo = useValidatorLogo(validator?.description.identity); return ( -
-
-
- Validator logo -
-
-
-
- {validator?.description.moniker || ""} -
- +
+
+ Validator logo +
+
+
+
+ {validator?.description.moniker || ""}
+
-
- -
-
- {validator - ? formatCommission(validator.commission.commissionRates.rate, 0) - : ""} -
-
- -
-
- { - if (!validator) return; - - staking.dispatch( - setModalOpened({ - content: { validator }, - type: "delegate", - }), - ); - }} - > - Delegate - - { - if (!validator) return; - - staking.dispatch( - setModalOpened({ - content: { validator }, - type: "undelegate", - }), - ); - }} - > - Undelegate (tmp) - -
-
+
+ +
+
+ {validator + ? formatCommission(validator.commission.commissionRates.rate, 0) + : ""} +
+
+ +
+
+ { + if (!validator) return; + + staking.dispatch( + setModalOpened({ + content: { validator }, + type: "delegate", + }), + ); + }} + > + Delegate + + { + if (!validator) return; + + staking.dispatch( + setModalOpened({ + content: { validator }, + type: "undelegate", + }), + ); + }} + > + Undelegate + +
); }; @@ -132,49 +159,40 @@ const UnbondingRow = ({ disabled, staking, unbonding }: UnbondingRowProps) => { const logo = useValidatorLogo(validator?.description.identity); return ( -
-
-
- Validator logo -
-
-
-
- {validator?.description.moniker || ""} -
- +
+
+ Validator logo +
+
+
+
+ {validator?.description.moniker || ""}
+
-
- -
-
Unbonding
-
- {new Date(unbonding.completionTime * 1000).toString()} -
-
- { - // @TODO - }} - > - Cancel - -
-
+
+ +
+
Unbonding
+
+ {formatUnbondingCompletionTime(unbonding.completionTime)} +
+
+ { + // @TODO + }} + > + Cancel + +
); }; @@ -183,6 +201,9 @@ type SortMethod = "bar" | "foo" | "none"; const HeaderTitle = HeaderTitleBase; +const headerStyle = + "grid w-full items-center justify-between gap-2 p-4 uppercase"; + const DelegationDetails = () => { const stakingRef = useStaking(); @@ -210,11 +231,8 @@ const DelegationDetails = () => { const sortedDelegations = delegations.items.slice(); return ( -
-
+
+
Delegations { const sortedUnbondings = unbondings.items.slice(); return ( -
-
+
+
Delegations { >
Staked Amount
- -
Commission
+ +
Status
-
Rewards
+
Completion Time
{sortedUnbondings.map((unbonding, index) => ( diff --git a/src/features/staking/components/main-page.tsx b/src/features/staking/components/main-page.tsx index 20c5654..9095701 100644 --- a/src/features/staking/components/main-page.tsx +++ b/src/features/staking/components/main-page.tsx @@ -1,21 +1,38 @@ "use client"; -import { memo } from "react"; +import { memo, useState } from "react"; import { Title } from "@/features/core/components/base"; +import { useStaking } from "../context/hooks"; +import DelegationDetails, { + DetailsTrigger, + getCanShowDetails, +} from "./delegation-details"; import StakingModals from "./staking-modals"; import StakingOverview from "./staking-overview"; import ValidatorsTable from "./validators-table"; function StakingPage() { + const { staking } = useStaking(); + const [isShowingDetails, setIsShowingDetails] = useState(false); + + const canShowDetail = getCanShowDetails(staking.state); + return ( <>
-
+
Your Staking Overview + {canShowDetail && ( + + )}
+ {isShowingDetails && canShowDetail && } Validators
diff --git a/src/features/staking/components/validator-delegation.tsx b/src/features/staking/components/validator-delegation.tsx index 2d74156..1cb3369 100644 --- a/src/features/staking/components/validator-delegation.tsx +++ b/src/features/staking/components/validator-delegation.tsx @@ -28,7 +28,10 @@ import { } from "../context/selectors"; import { getXionCoin } from "../lib/core/coins"; import { formatToSmallDisplay, formatXionToUSD } from "../lib/formatters"; -import DelegationDetails, { getCanShowDetails } from "./delegation-details"; +import DelegationDetails, { + DetailsTrigger, + getCanShowDetails, +} from "./delegation-details"; import { DivisorVertical } from "./divisor"; export default function ValidatorDelegation() { @@ -188,13 +191,10 @@ export default function ValidatorDelegation() {
My Delegations {canShowDetail && ( - + )}
{content} diff --git a/src/features/staking/components/validators-table.tsx b/src/features/staking/components/validators-table.tsx index 10ceaa3..4e15ff9 100644 --- a/src/features/staking/components/validators-table.tsx +++ b/src/features/staking/components/validators-table.tsx @@ -1,7 +1,7 @@ "use client"; import BigNumber from "bignumber.js"; -import { useState } from "react"; +import { memo, useState } from "react"; import { ButtonPill, NavLink } from "@/features/core/components/base"; import { HeaderTitleBase } from "@/features/core/components/table"; @@ -236,4 +236,4 @@ const ValidatorsTable = () => { ); }; -export default ValidatorsTable; +export default memo(ValidatorsTable); diff --git a/src/features/staking/lib/core/icons.ts b/src/features/staking/lib/core/icons.ts index fbe5fd1..09301bf 100644 --- a/src/features/staking/lib/core/icons.ts +++ b/src/features/staking/lib/core/icons.ts @@ -17,3 +17,9 @@ export const cross = [ '', "", ].join(""); + +export const pointer = [ + '', + '', + "", +].join(""); diff --git a/src/features/staking/lib/formatters.ts b/src/features/staking/lib/formatters.ts index 4f30852..87636be 100644 --- a/src/features/staking/lib/formatters.ts +++ b/src/features/staking/lib/formatters.ts @@ -61,3 +61,20 @@ export const formatXionToUSD = (coin: Coin | null, compact?: boolean) => { return `$${compact ? formatToSmallDisplay(usd) : usd.toFormat(2)}`; }; + +export const formatUnbondingCompletionTime = (completionTime: number) => { + const completionTimestamp = completionTime * 1000; + + const remainingDays = Math.floor( + (completionTimestamp - Date.now()) / (1000 * 60 * 60 * 24), + ); + + const month = new Date(completionTimestamp).toLocaleString("default", { + month: "short", + }); + + const day = new Date(completionTimestamp).getDate(); + const year = new Date(completionTimestamp).getFullYear(); + + return `in ${remainingDays} day${remainingDays === 1 ? "" : "s"}, ${month} ${day} ${year}`; +};