diff --git a/packages/web/src/components/dashboard/project-info-card.tsx b/packages/web/src/components/dashboard/project-info-card.tsx index ae596589..d2012dd0 100644 --- a/packages/web/src/components/dashboard/project-info-card.tsx +++ b/packages/web/src/components/dashboard/project-info-card.tsx @@ -128,7 +128,7 @@ export function ProjectInfoCard(props: Props) { {'Citizend token sale'} - {vestingStart && ( + {!!vestingStart && ( <> {'Vesting starting at:'} diff --git a/packages/web/src/components/screens/dashboard.tsx b/packages/web/src/components/screens/dashboard.tsx index e1f0d778..4c7699f8 100644 --- a/packages/web/src/components/screens/dashboard.tsx +++ b/packages/web/src/components/screens/dashboard.tsx @@ -9,6 +9,7 @@ import { ProjectInfoCard } from 'src/components/dashboard/project-info-card'; import { SaleForm } from 'src/components/dashboard/sale-form'; import { SaleState, useSale } from 'src/hooks/use-sale'; import { ShareReferralModal } from 'src/components/modals/share-referral-modal'; +import { UnknownDate } from 'src/components/unknown-date'; import { Vesting } from 'src/components/vesting'; import { Web3Provider } from '@ethersproject/providers'; import { currencyConfig } from 'src/core/constants'; @@ -72,7 +73,9 @@ export function DashboardScreen() { myContribution={formatCurrency(balance, currencyConfig.aUsd)} price={formatCurrency(price, currencyConfig.aUsd)} raised={formatCompactNumber(raised, currencyConfig.aUsd)} - vestingStart={formatFromUnix(vestingStart)} + vestingStart={ + vestingStart === 0 ? undefined : formatFromUnix(vestingStart) + } /> {!isLoading && state === 'COUNTDOWN' && !isNaN(vestingStart) && ( @@ -82,6 +85,7 @@ export function DashboardScreen() { /> )} + {!isLoading && state === 'VESTING_UNKNOWN' && } {!isLoading && state === 'SALE' && ( )} diff --git a/packages/web/src/components/unknown-date.tsx b/packages/web/src/components/unknown-date.tsx new file mode 100644 index 00000000..1b528d1d --- /dev/null +++ b/packages/web/src/components/unknown-date.tsx @@ -0,0 +1,64 @@ +/** + * Module dependencies. + */ + +import { Text } from 'src/components/core/text'; +import React from 'react'; +import styled from 'styled-components'; + +/** + * `Section` styled component. + */ + +const Section = styled.section` + max-width: 100%; + overflow: hidden; + position: relative; +`; + +/** + * `Container` styled component. + */ + +const Container = styled.section` + margin: 0 auto; + max-width: 640px; + padding: 3rem 2rem 0; + text-align: center; +`; + +/** + * `Title` styled component. + */ + +const Title = styled(Text).attrs({ variant: 'lead' })` + display: block; + margin-bottom: 3rem; +`; + +/** + * `Label` styled component. + */ + +const Label = styled(Text).attrs({ variant: 'body' })` + margin: 0; + padding-top: 0.75rem; +`; + +/** + * Export `UnknownDate` component. + */ + +export function UnknownDate() { + return ( +
+ + + {'Vesting will begin after the public sale has concluded'} + + + + +
+ ); +} diff --git a/packages/web/src/components/vesting/index.tsx b/packages/web/src/components/vesting/index.tsx index c1db484f..4bb9ae7d 100644 --- a/packages/web/src/components/vesting/index.tsx +++ b/packages/web/src/components/vesting/index.tsx @@ -10,7 +10,6 @@ import { formatCurrency } from 'src/core/utils/formatters'; import { media } from 'src/styles/breakpoints'; import { useClaim, useRefund, useVesting } from 'src/hooks/use-vesting'; import React, { useMemo } from 'react'; -import dayjs from 'src/core/utils/dayjs'; import styled from 'styled-components'; /** @@ -28,19 +27,6 @@ const Grid = styled.section` `} `; -/** - * `getFirstDayOfNextMonth`. - */ - -function getFirstDayOfNextMonth() { - const now = dayjs(); - - return now - .month(now.month() + 1) - .date(1) - .format('DD/MM/YYYY'); -} - /** * Export `Vesting` component. */ @@ -65,7 +51,7 @@ export function Vesting() { return ( diff --git a/packages/web/src/core/constants.ts b/packages/web/src/core/constants.ts index a8dca19e..f12356bf 100644 --- a/packages/web/src/core/constants.ts +++ b/packages/web/src/core/constants.ts @@ -8,7 +8,7 @@ export const currencyConfig = { decimalPlaces: 12, decimalPlacesToDisplay: 6, skipTrailingZeros: true, - symbol: '$' + symbol: undefined }, ctnd: { currency: 'CTND', diff --git a/packages/web/src/core/utils/formatters.ts b/packages/web/src/core/utils/formatters.ts index fa791d7b..f1202955 100644 --- a/packages/web/src/core/utils/formatters.ts +++ b/packages/web/src/core/utils/formatters.ts @@ -58,13 +58,54 @@ export type DateTimeOptions = { hideHour?: boolean; }; +/** + * `replaceCryptoSymbol` util. + */ + +function replaceCryptoSymbol(value: string, options: CurrencyOptions) { + const { currency, symbol } = options; + const currencySymbol = symbol ?? currency; + const whitespace = size(symbol ?? currency) > 1 ? ' ' : ''; + + return value + .replace(/^BTC */, `${currencySymbol}${whitespace}`) + .replace(/^-BTC */, `${currencySymbol}${whitespace}`) + .replace(/ *BTC$/, `${whitespace}${currencySymbol}`); +} + +/** + * `formatCryptoParts` util. + */ + +function formatCryptoParts( + formattedParts: Intl.NumberFormatPart[], + options: CurrencyOptions +) { + const [currencyPart, literalPart, ...rest] = formattedParts; + + // Force currency to the right side. + if (currencyPart.type === 'currency' && literalPart.type === 'literal') { + const switchedParts = [ + ...rest.map(({ value }) => value), + literalPart?.value, + currencyPart?.value + ]; + + return replaceCryptoSymbol(switchedParts.join(''), options); + } + + return replaceCryptoSymbol( + formattedParts.map(({ value }) => value).join(''), + options + ); +} + /** * Currency formatter. */ function currencyFormatter(options: NumberOptions): Intl.NumberFormat { const { decimalPlacesToDisplay, skipTrailingZeros, ...rest } = options; - const maximumFractionDigits = Math.min( decimalPlacesToDisplay ?? 2, maximumDecimalPlaces @@ -168,12 +209,7 @@ export function formatCurrency( style: 'currency' }); - return formattedParts - .map(({ value }) => value) - .join('') - .replace(/^BTC */, `${currencySymbol}${whitespace}`) - .replace(/^-BTC */, `${currencySymbol}${whitespace}`) - .replace(/ *BTC$/, `${whitespace}${currencySymbol}`); + return formatCryptoParts(formattedParts, options); } catch (error) { return fallback; } @@ -188,7 +224,7 @@ export function formatCompactNumber( value: NullableNumber, options?: CurrencyOptions ): string { - const { currency, decimalPlacesToDisplay = 1, symbol } = options ?? {}; + const { currency, decimalPlacesToDisplay = 1 } = options ?? {}; const numericValue = convertNumberToString(value ?? '0'); const [integer, fraction = ''] = numericValue.split('.'); const compactSignificantDigits = integer.length % 3 || 3; @@ -210,19 +246,15 @@ export function formatCompactNumber( style: currency ? 'currency' : undefined }).format(truncatedValue); } catch (error) { - const whitespace = size(symbol ?? currency) > 1 ? ' ' : ''; - - return new Intl.NumberFormat(locale, { + const formattedParts = new Intl.NumberFormat(locale, { compactDisplay: 'short', currency: 'BTC', maximumSignificantDigits, notation: 'compact', style: currency ? 'currency' : undefined - }) - .format(truncatedValue) - .replace(/^BTC */, `${symbol ?? currency}${whitespace}`) - .replace(/^-BTC */, `${symbol ?? currency}${whitespace}`) - .replace(/ *BTC$/, `${whitespace}${symbol ?? currency}`); + }).formatToParts(truncatedValue); + + return formatCryptoParts(formattedParts, options); } } diff --git a/packages/web/src/hooks/use-app-status.ts b/packages/web/src/hooks/use-app-status.ts index 07db8d56..751dd71e 100644 --- a/packages/web/src/hooks/use-app-status.ts +++ b/packages/web/src/hooks/use-app-status.ts @@ -17,7 +17,8 @@ const appState = { comingSoon: 'SOON', countdown: 'COUNTDOWN', sale: 'SALE', - vesting: 'VESTING' + vesting: 'VESTING', + vestingUnknown: 'VESTING_UNKNOWN' } as const; /** @@ -106,16 +107,20 @@ export function useAppStatus() { vestingStart: !!vestingStart?.toNumber && vestingStart.toNumber() }); - if (dayjs().isAfter(vestingStartDate)) { - return getStatus('VESTING'); + if (dayjs().isAfter(saleStartDate) && dayjs().isBefore(saleEndDate)) { + return getStatus('SALE'); + } + + if (vestingStart.eq(0)) { + return getStatus('VESTING_UNKNOWN'); } if (dayjs().isAfter(saleEndDate) && dayjs().isBefore(vestingStartDate)) { return getStatus('COUNTDOWN'); } - if (dayjs().isAfter(saleStartDate) && dayjs().isBefore(saleEndDate)) { - return getStatus('SALE'); + if (dayjs().isAfter(vestingStartDate)) { + return getStatus('VESTING'); } return getStatus('SOON'); diff --git a/packages/web/src/hooks/use-vesting.ts b/packages/web/src/hooks/use-vesting.ts index 56416d25..d82bf1fa 100644 --- a/packages/web/src/hooks/use-vesting.ts +++ b/packages/web/src/hooks/use-vesting.ts @@ -12,6 +12,7 @@ import { import { useCallback, useEffect, useState } from 'react'; import { useContracts } from 'src/context/contracts'; import { useWeb3React } from '@web3-react/core'; +import dayjs from 'src/core/utils/dayjs'; /** * Export `VestingState` type. @@ -27,6 +28,19 @@ export type VestingState = { tokens: string; }; +/** + * `getFirstDayOfNextMonth`. + */ + +function getFirstDayOfNextMonth() { + const now = dayjs(); + + return now + .month(now.month() + 1) + .date(1) + .format('DD/MM/YYYY'); +} + /** * Export `useVesting` hook. */ @@ -45,25 +59,23 @@ export function useVesting() { const { account } = useWeb3React(); const contracts = useContracts(); const getVestingState = useCallback(async () => { - if (!contracts?.vesting) { + if (!contracts?.vesting || !contracts?.sale1 || !contracts?.citizend) { return; } try { + const claimableTotal = await contracts.vesting.claimable(account); const claimed = await contracts.vesting.claimed(account); - const claimablePublic = await contracts.vesting.claimablePublic(account); + const refundable = await contracts.sale1.refundAmount(account); const tokens = await contracts.citizend.balanceOf(account); - const claimablePrivate = await contracts.vesting.claimablePrivate( - account - ); setState({ alreadyClaimed: claimed.toString(), - claimEnabled: claimablePublic.gt(0), - claimable: claimablePublic.toString(), - nextRelease: 'TODO', - refundEnabled: claimablePrivate.gt(0), - refundable: claimablePrivate.toString(), + claimEnabled: claimableTotal.gt(0), + claimable: claimableTotal.toString(), + nextRelease: getFirstDayOfNextMonth(), + refundEnabled: refundable.gt(0), + refundable: refundable.toString(), tokens: tokens.toString() }); } catch (error) {