Skip to content

Commit

Permalink
feat: continue with delegation details
Browse files Browse the repository at this point in the history
  • Loading branch information
icfor committed Mar 5, 2024
1 parent f58bd6c commit 846d910
Show file tree
Hide file tree
Showing 10 changed files with 497 additions and 152 deletions.
6 changes: 3 additions & 3 deletions src/features/core/components/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ export const InputBox = ({ error, ...props }: InputProps) => (
<input
{...props}
className={[
"h-[96px] w-full rounded-[16px] border-[1px] bg-black p-[16px] pl-[52px] text-[48px] focus:outline-none",
"h-[96px] w-full rounded-[16px] border-[1px] bg-black p-[16px] pr-[140px] text-[48px] focus:outline-none",
error ? "border-danger" : "border-white",
].join(" ")}
/>
<span className="absolute bottom-0 left-[12px] top-0 flex h-full items-center text-[24px] text-[48px]">
$
<span className="absolute bottom-0 right-[12px] top-0 flex h-full items-center text-[24px] text-[48px] text-typo-300">
XION
</span>
</span>
);
Expand Down
52 changes: 52 additions & 0 deletions src/features/core/components/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { PropsWithChildren } from "react";

import { chevron } from "@/features/staking/lib/core/icons";

type Props<SortMethod> = PropsWithChildren & {
onSort?: (method: SortMethod) => void;
sort?: SortMethod;
sorting?: [string, string];
};

export const HeaderTitleBase = <SortMethod extends string>({
children,
onSort,
sort,
sorting,
}: Props<SortMethod>) => {
const sortingOrder = ((sorting || []) as string[]).concat(["none"]);
const sortingIndex = sort ? sortingOrder.indexOf(sort) : -1;

return (
<div className="text-[14px] font-normal leading-[14px] tracking-wider">
<span className="relative">
{children}{" "}
{!!onSort && !!sort && (
<button
className="absolute right-[-16px] top-[6px] cursor-pointer"
dangerouslySetInnerHTML={{ __html: chevron }}
onClick={() => {
if (!onSort) return;

const nextIndex =
(1 + sortingOrder.indexOf(sort)) % sortingOrder.length;

const nextSorting = sortingOrder[nextIndex];

onSort?.(nextSorting as SortMethod);
}}
style={{
rotate: (() => {
if (sortingIndex === 0) {
return "180deg";
}

return sortingIndex === 1 ? "0deg" : "90deg";
})(),
}}
/>
)}
</span>
</div>
);
};
300 changes: 300 additions & 0 deletions src/features/staking/components/delegation-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import { memo, useState } from "react";

import { ButtonPill } from "@/features/core/components/base";
import { HeaderTitleBase } from "@/features/core/components/table";

import { useStaking } from "../context/hooks";
import { setModalOpened } from "../context/reducer";
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 AddressShort from "./address-short";
import TokenColors from "./token-colors";

export const getCanShowDetails = (state: StakingState) => {
const userTotalUnbondings = getTotalUnbonding(state, null);
const userTotalDelegation = getTotalDelegation(state, null);

return (
coinIsPositive(userTotalUnbondings) || coinIsPositive(userTotalDelegation)
);
};

const gridStyle = {
gap: "16px",
gridTemplateColumns: "60px 1.5fr repeat(4, 1fr)",
};

type DelegationRowProps = {
delegation: NonNullable<StakingState["delegations"]>["items"][number];
disabled?: boolean;
staking: StakingContextType;
};

const DelegationRow = ({
delegation,
disabled,
staking,
}: DelegationRowProps) => {
const validator = (staking.state.validators?.items || []).find(
(v) => v.operatorAddress === delegation.validatorAddress,
);

const logo = useValidatorLogo(validator?.description.identity);

return (
<div className="flex w-full flex-col items-center justify-between gap-2">
<div
className="grid w-full items-center justify-between gap-2 p-4"
style={gridStyle}
>
<div className="flex items-center justify-start">
<img
alt="Validator logo"
className="block w-[50px] rounded-full"
src={logo}
style={{ height: 50, width: 50 }}
/>
</div>
<div className="flex flex-1 flex-row justify-start gap-4">
<div className="flex flex-col justify-start gap-2 text-left">
<div className="text-[14px] font-bold leading-[20px]">
{validator?.description.moniker || ""}
</div>
<AddressShort address={validator?.operatorAddress || ""} />
</div>
</div>
<div className="text-right">
<TokenColors text={formatCoin(delegation.balance)} />
</div>
<div className="text-right">
{validator
? formatCommission(validator.commission.commissionRates.rate, 0)
: ""}
</div>
<div className="text-right">
<TokenColors text={formatCoin(delegation.rewards)} />
</div>
<div>
<ButtonPill
disabled={disabled}
onClick={() => {
if (!validator) return;

staking.dispatch(
setModalOpened({
content: { validator },
type: "delegate",
}),
);
}}
>
Delegate
</ButtonPill>
<ButtonPill
disabled={disabled}
onClick={() => {
if (!validator) return;

staking.dispatch(
setModalOpened({
content: { validator },
type: "undelegate",
}),
);
}}
>
Undelegate (tmp)
</ButtonPill>
</div>
</div>
<div
className="box-content h-[1px] bg-bg-500"
style={{ width: "calc(100% - 48px)" }}
/>
</div>
);
};

type UnbondingRowProps = {
disabled?: boolean;
staking: StakingContextType;
unbonding: NonNullable<StakingState["unbondings"]>["items"][number];
};

const UnbondingRow = ({ disabled, staking, unbonding }: UnbondingRowProps) => {
const validator = (staking.state.validators?.items || []).find(
(v) => v.operatorAddress === unbonding.validator,
);

const logo = useValidatorLogo(validator?.description.identity);

return (
<div className="flex w-full flex-col items-center justify-between gap-2">
<div
className="grid w-full items-center justify-between gap-2 p-4"
style={gridStyle}
>
<div className="flex items-center justify-start">
<img
alt="Validator logo"
className="block w-[50px] rounded-full"
src={logo}
style={{ height: 50, width: 50 }}
/>
</div>
<div className="flex flex-1 flex-row justify-start gap-4">
<div className="flex flex-col justify-start gap-2 text-left">
<div className="text-[14px] font-bold leading-[20px]">
{validator?.description.moniker || ""}
</div>
<AddressShort address={validator?.operatorAddress || ""} />
</div>
</div>
<div className="text-right">
<TokenColors text={formatCoin(unbonding.balance)} />
</div>
<div className="text-right">Unbonding</div>
<div className="text-right">
{new Date(unbonding.completionTime * 1000).toString()}
</div>
<div>
<ButtonPill
disabled={disabled}
onClick={() => {
// @TODO
}}
>
Cancel
</ButtonPill>
</div>
</div>
<div
className="box-content h-[1px] bg-bg-500"
style={{ width: "calc(100% - 48px)" }}
/>
</div>
);
};

type SortMethod = "bar" | "foo" | "none";

const HeaderTitle = HeaderTitleBase<SortMethod>;

const DelegationDetails = () => {
const stakingRef = useStaking();

const { staking } = stakingRef;

const [delegationsSortMethod, setDelegationsSortMethod] =
useState<SortMethod>("none");

const [unbondingsSortMethod, setUnbondingsSortMethod] =
useState<SortMethod>("none");

const { delegations, unbondings } = staking.state;

const hasDelegations = !!delegations?.items.length;
const hasUnbondings = !!unbondings?.items.length;

if (!hasDelegations && !hasUnbondings) {
return null;
}

return (
<div className="flex h-full flex-1 flex-col items-end gap-[16px]">
{hasDelegations &&
(() => {
const sortedDelegations = delegations.items.slice();

return (
<div className="w-full overflow-hidden rounded-[24px] bg-bg-600 pb-4 text-typo-100">
<div
className="grid w-full items-center justify-between gap-2 bg-bg-500 p-4 uppercase"
style={gridStyle}
>
<div />
<HeaderTitle>Delegations</HeaderTitle>
<HeaderTitle
onSort={setUnbondingsSortMethod}
sort={delegationsSortMethod}
sorting={["staked-asc", "staked-desc"]}
>
<div className="text-right">Staked Amount</div>
</HeaderTitle>
<HeaderTitle
onSort={setUnbondingsSortMethod}
sort={delegationsSortMethod}
sorting={["commission-asc", "commission-desc"]}
>
<div className="text-right">Commission</div>
</HeaderTitle>
<HeaderTitle
onSort={setUnbondingsSortMethod}
sort={delegationsSortMethod}
sorting={["rewards-asc", "rewards-desc"]}
>
<div className="text-right">Rewards</div>
</HeaderTitle>
</div>
{sortedDelegations.map((delegation, index) => (
<DelegationRow
delegation={delegation}
key={index}
staking={staking}
/>
))}
</div>
);
})()}
{hasUnbondings &&
(() => {
const sortedUnbondings = unbondings.items.slice();

return (
<div className="w-full overflow-hidden rounded-[24px] bg-bg-600 pb-4 text-typo-100">
<div
className="grid w-full items-center justify-between gap-2 bg-bg-500 p-4 uppercase"
style={gridStyle}
>
<div />
<HeaderTitle>Delegations</HeaderTitle>
<HeaderTitle
onSort={setDelegationsSortMethod}
sort={unbondingsSortMethod}
sorting={["staked-asc", "staked-desc"]}
>
<div className="text-right">Staked Amount</div>
</HeaderTitle>
<HeaderTitle
onSort={setDelegationsSortMethod}
sort={unbondingsSortMethod}
sorting={["commission-asc", "commission-desc"]}
>
<div className="text-right">Commission</div>
</HeaderTitle>
<HeaderTitle
onSort={setDelegationsSortMethod}
sort={unbondingsSortMethod}
sorting={["rewards-asc", "rewards-desc"]}
>
<div className="text-right">Rewards</div>
</HeaderTitle>
</div>
{sortedUnbondings.map((unbonding, index) => (
<UnbondingRow
key={index}
staking={staking}
unbonding={unbonding}
/>
))}
</div>
);
})()}
</div>
);
};

export default memo(DelegationDetails);
Loading

0 comments on commit 846d910

Please sign in to comment.