diff --git a/src/hooks/useCountdown.ts b/src/hooks/useCountdown.ts new file mode 100644 index 0000000..71a3bf9 --- /dev/null +++ b/src/hooks/useCountdown.ts @@ -0,0 +1,10 @@ +import { relativeDateFormat } from '@i18n'; + +import { useTicker } from './useTicker'; + +export function useCountdown({ timestamp }: { timestamp?: Date | string | number | undefined }) { + const curTimestamp = useTicker(() => Date.now(), 1000); + const timeLeft = timestamp ? relativeDateFormat(curTimestamp, timestamp) : undefined; + + return timeLeft; +} diff --git a/src/i18n/dateFormat.ts b/src/i18n/dateFormat.ts index 10632f4..5e647c1 100644 --- a/src/i18n/dateFormat.ts +++ b/src/i18n/dateFormat.ts @@ -4,7 +4,7 @@ export function dateFormat( value: Date | string | number | undefined, tpl: 'dateTime' | 'date' | string = 'date', ) { - if (!value) return null; + if (!value) return undefined; if (tpl === 'dateTime') { tpl = 'dd.MM.yyyy HH:mm:ss'; @@ -12,10 +12,10 @@ export function dateFormat( tpl = 'dd.MM.yyyy'; } - if (value.valueOf() == 0) return null; + if (value.valueOf() == 0) return undefined; const date = new Date(value); - if (!isValid(date)) return null; + if (!isValid(date)) return undefined; return format(new Date(value), tpl); } diff --git a/src/pages/DashboardPage/Summary.tsx b/src/pages/DashboardPage/Summary.tsx index 5449700..b4d0b8d 100644 --- a/src/pages/DashboardPage/Summary.tsx +++ b/src/pages/DashboardPage/Summary.tsx @@ -1,6 +1,5 @@ import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; -import { relativeDateFormat } from '@i18n'; import { bytesFormatter, numberWithCommasFormatter, @@ -26,7 +25,7 @@ import { useNetworkSummary } from '@api/subsquid-network-squid'; import SquaredChip from '@components/Chip/SquaredChip'; import { HelpTooltip } from '@components/HelpTooltip'; import { Loader } from '@components/Loader'; -import { useTicker } from '@hooks/useTicker'; +import { useCountdown } from '@hooks/useCountdown'; import { useContracts } from '@network/useContracts'; export function ColumnLabel({ children, color }: PropsWithChildren<{ color?: string }>) { @@ -108,14 +107,13 @@ function OnlineInfo() { } function CurrentEpochEstimation({ epochEnd }: { epochEnd: number }) { - const curTime = useTicker(() => Date.now(), 1000); - const epochEndsIn = useMemo(() => relativeDateFormat(curTime, epochEnd), [curTime, epochEnd]); + const timeLeft = useCountdown({ timestamp: epochEnd }); return ( Ends in ~{epochEndsIn}} + label={~{timeLeft}} color="warning" /> diff --git a/src/pages/GatewaysPage/GatewayUnstake.tsx b/src/pages/GatewaysPage/GatewayUnstake.tsx index 6453ac2..fb93f14 100644 --- a/src/pages/GatewaysPage/GatewayUnstake.tsx +++ b/src/pages/GatewaysPage/GatewayUnstake.tsx @@ -1,8 +1,9 @@ import React, { useState } from 'react'; -import { LockOpen as LockOpenIcon } from '@mui/icons-material'; +import { dateFormat } from '@i18n'; +import { Lock } from '@mui/icons-material'; import { LoadingButton } from '@mui/lab'; -import { SxProps } from '@mui/material'; +import { Box, SxProps, Tooltip } from '@mui/material'; import toast from 'react-hot-toast'; import { useClient } from 'wagmi'; import * as yup from 'yup'; @@ -11,6 +12,7 @@ import { gatewayRegistryAbi } from '@api/contracts'; import { useWriteSQDTransaction } from '@api/contracts/useWriteTransaction'; import { errorMessage } from '@api/contracts/utils'; import { ContractCallDialog } from '@components/ContractCallDialog'; +import { useCountdown } from '@hooks/useCountdown'; import { useSquidHeight } from '@hooks/useSquidNetworkHeightHooks'; import { useContracts } from '@network/useContracts'; @@ -24,22 +26,66 @@ export const stakeSchema = yup.object({ // .max(yup.ref('max'), ({ max }) => `Amount should be less than ${formatSqd(max)} `), }); -export function GatewayUnstakeButton({ sx, disabled }: { sx?: SxProps; disabled?: boolean }) { +function UnlocksTooltip({ timestamp }: { timestamp?: Date | string | number | undefined }) { + const timeLeft = useCountdown({ timestamp }); + + return `Unlocks in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`; +} + +export function GatewayUnstakeButton({ + sx, + disabled, + source, +}: { + sx?: SxProps; + disabled?: boolean; + source: { + locked: boolean; + unlockedAt?: string; + }; +}) { const [open, setOpen] = useState(false); return ( <> - } - disabled={disabled} - loading={open} - variant="contained" - color="error" - onClick={() => setOpen(true)} - sx={sx} + + ) : ( + 'Auto-extension is enabled' + ) + } + placement="top" > - WITHDRAW - + + {source.locked && !disabled && ( + + )} + } + disabled={disabled || source.locked} + loading={open} + variant="contained" + color="error" + onClick={() => setOpen(true)} + sx={sx} + > + WITHDRAW + + + setOpen(false)} /> ); diff --git a/src/pages/GatewaysPage/GatewaysPage.tsx b/src/pages/GatewaysPage/GatewaysPage.tsx index 762aa33..745673c 100644 --- a/src/pages/GatewaysPage/GatewaysPage.tsx +++ b/src/pages/GatewaysPage/GatewaysPage.tsx @@ -9,6 +9,7 @@ import { Avatar, Box, Button, + Card, Collapse, Divider, IconButton, @@ -44,6 +45,7 @@ import { import SquaredChip from '@components/Chip/SquaredChip'; import { HelpTooltip } from '@components/HelpTooltip'; import { DashboardTable, NoItems } from '@components/Table'; +import { useCountdown } from '@hooks/useCountdown'; import { CenteredPageWrapper } from '@layouts/NetworkLayout'; import { ConnectedWalletRequired } from '@network/ConnectedWalletRequired'; import { useAccount } from '@network/useAccount'; @@ -57,6 +59,18 @@ import { GatewayStakeButton } from './GatewayStake'; import { GatewayUnregisterButton } from './GatewayUnregister'; import { GatewayUnstakeButton } from './GatewayUnstake'; +function AppliesTooltip({ timestamp }: { timestamp?: string }) { + const timeLeft = useCountdown({ timestamp }); + + return {`Applies in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`}; +} + +function ExpiresTooltip({ timestamp }: { timestamp?: string }) { + const timeLeft = useCountdown({ timestamp }); + + return {`Expires in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`}; +} + export function MyStakes() { const theme = useTheme(); const narrowXs = useMediaQuery(theme.breakpoints.down('xs')); @@ -95,6 +109,14 @@ export function MyStakes() { const { data: lastL1Block, isLoading: isL1BlockLoading } = useBlock({ chainId: l1ChainId, }); + const { data: appliedAtL1Block, isLoading: isAppliedAtBlockLoading } = useBlock({ + chainId: l1ChainId, + blockNumber: stake?.lockStart, + includeTransactions: false, + query: { + enabled: stake && stake?.lockStart <= (lastL1Block?.number || 0n), + }, + }); const { data: unlockedAtL1Block, isLoading: isUnlockedAtBlockLoading } = useBlock({ chainId: l1ChainId, blockNumber: stake?.lockEnd, @@ -124,15 +146,27 @@ export function MyStakes() { stake.lockEnd >= (lastL1Block?.number || 0n); const isExpired = !!stake?.amount && stake.lockEnd < (lastL1Block?.number || 0n); - const unlockDate = useMemo(() => { + const appliedAt = useMemo(() => { if (!stake || !lastL1Block) return; + if (stake.lockStart < lastL1Block.number) + return new Date(Number(appliedAtL1Block?.timestamp || 0n) * 1000).toISOString(); + + return new Date( + Number(lastL1Block.timestamp) * 1000 + + getBlockTime(stake.lockStart - lastL1Block.number + 1n), + ).toISOString(); + }, [appliedAtL1Block?.timestamp, lastL1Block, stake]); + + const unlockedAt = useMemo(() => { + if (!stake || !lastL1Block || stake.autoExtension) return; + if (stake.lockEnd < lastL1Block.number) - return Number(unlockedAtL1Block?.timestamp || 0n) * 1000; + return new Date(Number(unlockedAtL1Block?.timestamp || 0n) * 1000).toISOString(); - return ( - Number(lastL1Block.timestamp) * 1000 + getBlockTime(stake.lockEnd - lastL1Block.number + 1n) - ); + return new Date( + Number(lastL1Block.timestamp) * 1000 + getBlockTime(stake.lockEnd - lastL1Block.number + 1n), + ).toISOString(); }, [lastL1Block, stake, unlockedAtL1Block?.timestamp]); const cuPerEpoch = useMemo(() => { @@ -154,7 +188,13 @@ export function MyStakes() { action={ - + } > @@ -181,14 +221,14 @@ export function MyStakes() { lastL1Block && (isPending ? ( } placement="top" > ) : isActive ? ( } placement="top" > @@ -210,7 +250,7 @@ export function MyStakes() { {numberWithCommasFormatter(cuPerEpoch || 0)} - } spacing={1} flex={1}> + {/* } spacing={1} flex={1}> Expired At @@ -221,7 +261,7 @@ export function MyStakes() { : 'Auto-extension enabled'} - + */} @@ -352,25 +392,28 @@ const GettingStarted = () => { ]; return ( - } - action={ - setOpen(!open)}> - - - } - > - Getting started with your portal + + } + action={ + + + + } + onClick={() => setOpen(!open)} + > + Getting started with your portal + - - + + {steps.map(({ primary, secondary }, i) => ( { - + ); }; diff --git a/src/pages/WorkersPage/WorkerStatus.tsx b/src/pages/WorkersPage/WorkerStatus.tsx index e2c3339..d731abb 100644 --- a/src/pages/WorkersPage/WorkerStatus.tsx +++ b/src/pages/WorkersPage/WorkerStatus.tsx @@ -1,12 +1,12 @@ import { useMemo } from 'react'; -import { dateFormat, relativeDateFormat } from '@i18n'; +import { dateFormat } from '@i18n'; import { CircleRounded } from '@mui/icons-material'; import { Box, Chip as MaterialChip, Tooltip, chipClasses, styled } from '@mui/material'; import capitalize from 'lodash-es/capitalize'; import { WorkerStatus as Status, Worker } from '@api/subsquid-network-squid'; -import { useTicker } from '@hooks/useTicker'; +import { useCountdown } from '@hooks/useCountdown'; export const Chip = styled(MaterialChip)(({ theme }) => ({ // [`&.${chipClasses.colorSuccess}`]: { @@ -23,6 +23,12 @@ export const Chip = styled(MaterialChip)(({ theme }) => ({ }, })); +function AppliesTooltip({ timestamp }: { timestamp?: string }) { + const timeLeft = useCountdown({ timestamp }); + + return `Applies in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`; +} + export function WorkerStatusChip({ worker, }: { @@ -55,20 +61,9 @@ export function WorkerStatusChip({ return { label: capitalize(worker.status), color: 'primary' }; }, [worker.jailReason, worker.jailed, worker.online, worker.status]); - const curTimestamp = useTicker(() => Date.now(), 1000); - const timeLeft = useMemo( - () => - worker.statusChangeAt ? relativeDateFormat(curTimestamp, worker.statusChangeAt) : undefined, - [curTimestamp, worker.statusChangeAt], - ); - const chip = ( } placement="top" > {`Unlocks in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`}; +} + export type SourceWalletWithDelegation = SourceWalletWithBalance & { locked: boolean; unlockedAt?: string; @@ -58,8 +64,7 @@ export function WorkerUndelegate({ const isLocked = useMemo(() => !!sources?.length && !sources?.some(d => !d.locked), [sources]); - const curTimestamp = useTicker(() => Date.now(), 1000); - const { unlockedAt, timeLeft } = useMemo(() => { + const { unlockedAt } = useMemo(() => { const min = sources?.reduce( (r, d) => { if (!d.unlockedAt) return r; @@ -72,18 +77,14 @@ export function WorkerUndelegate({ return { unlockedAt: min ? new Date(min).toISOString() : undefined, - timeLeft: min ? relativeDateFormat(curTimestamp, min) : undefined, }; - }, [curTimestamp, sources]); + }, [sources]); return ( <> - - {isLocked && ( - + } placement="top"> + + {isLocked && !disabled && ( - - )} - setOpen(true)} - variant="outlined" - color="error" - disabled={disabled || isLocked} - > - UNDELEGATE - - + )} + setOpen(true)} + variant="outlined" + color="error" + disabled={disabled || isLocked} + > + UNDELEGATE + + + setOpen(false)} diff --git a/src/pages/WorkersPage/WorkerWithdraw.tsx b/src/pages/WorkersPage/WorkerWithdraw.tsx index e2b3e6f..7653e9a 100644 --- a/src/pages/WorkersPage/WorkerWithdraw.tsx +++ b/src/pages/WorkersPage/WorkerWithdraw.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { dateFormat, relativeDateFormat } from '@i18n'; +import { dateFormat } from '@i18n'; import { peerIdToHex } from '@lib/network'; import { Lock } from '@mui/icons-material'; import { LoadingButton } from '@mui/lab'; @@ -13,10 +13,17 @@ import { useWriteSQDTransaction } from '@api/contracts/useWriteTransaction'; import { errorMessage } from '@api/contracts/utils'; import { AccountType, SourceWallet, Worker } from '@api/subsquid-network-squid'; import { ContractCallDialog } from '@components/ContractCallDialog'; +import { useCountdown } from '@hooks/useCountdown'; import { useSquidHeight } from '@hooks/useSquidNetworkHeightHooks'; import { useAccount } from '@network/useAccount'; import { useContracts } from '@network/useContracts'; +function UnlocksTooltip({ timestamp }: { timestamp?: string }) { + const timeLeft = useCountdown({ timestamp }); + + return {`Unlocks in ${timeLeft} (${dateFormat(timestamp, 'dateTime')})`}; +} + export function WorkerWithdrawButton({ worker, source, @@ -35,15 +42,12 @@ export function WorkerWithdrawButton({ return ( <> - - {source.locked && ( - + } + placement="top" + > + + {source.locked && !disabled && ( - - )} - setOpen(true)} - variant="outlined" - color="error" - disabled={disabled || source.locked} - > - WITHDRAW - - + )} + setOpen(true)} + variant="outlined" + color="error" + disabled={disabled || source.locked} + > + WITHDRAW + + + setOpen(false)}