diff --git a/apps/dapp/assets/icons/connect.svg b/apps/dapp/assets/icons/connect.svg
deleted file mode 100644
index 9128d6da6..000000000
--- a/apps/dapp/assets/icons/connect.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/apps/dapp/assets/icons/wallet.svg b/apps/dapp/assets/icons/wallet.svg
deleted file mode 100644
index 55800008e..000000000
--- a/apps/dapp/assets/icons/wallet.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/apps/dapp/atoms/localStorageEffect.ts b/apps/dapp/atoms/localStorageEffect.ts
index 5dc13c470..c71588593 100644
--- a/apps/dapp/atoms/localStorageEffect.ts
+++ b/apps/dapp/atoms/localStorageEffect.ts
@@ -2,7 +2,7 @@ import { AtomEffect } from 'recoil'
export const localStorageEffect: (key: string) => AtomEffect =
(key) =>
- ({ setSelf, onSet, node }) => {
+ ({ setSelf, onSet, node: _ }) => {
const savedValue = localStorage.getItem(key)
if (savedValue != null) {
const json = JSON.parse(savedValue)
diff --git a/apps/dapp/atoms/proposals.ts b/apps/dapp/atoms/proposals.ts
index 1b235cc77..37364e3fa 100644
--- a/apps/dapp/atoms/proposals.ts
+++ b/apps/dapp/atoms/proposals.ts
@@ -1,8 +1,6 @@
import { atom, atomFamily } from 'recoil'
-import { ContractProposalMap, ExtendedProposalResponse } from 'types/proposals'
-
-import { localStorageEffect } from './localStorageEffect'
+import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao'
// By depending on this atom, the selector for retrieving the list
// of on-chain proposals can know when it's time to actually re-calculate
@@ -22,7 +20,7 @@ export const proposalsRequestStartBeforeAtom = atom({
})
// The loaded list of proposals.
-export const proposalListAtom = atomFamily({
+export const proposalListAtom = atomFamily({
key: 'proposalList',
default: [],
})
@@ -54,21 +52,3 @@ export const proposalsUpdated = atomFamily({
key: 'proposalsUpdatedAtom',
default: [],
})
-
-// The next numeric ID for creating draft proposals. Saved to localstorage
-// and incremented so that we don't end up with multiple draft proposals and
-// multisigs with the same ID.
-export const nextDraftProposalIdAtom = atom({
- key: 'nextDraftProposalId',
- default: 10000,
- effects_UNSTABLE: [localStorageEffect('nextDraftProposalId')],
-})
-
-// The map of draft proposals associated with a given contract.
-export const contractProposalMapAtom = atom({
- key: 'contractProposalMap',
- default: {},
- effects_UNSTABLE: [
- localStorageEffect('contractProposalMap'),
- ],
-})
diff --git a/apps/dapp/components/BetaWarning.tsx b/apps/dapp/components/BetaWarning.tsx
index fa7eba027..87f7c971c 100644
--- a/apps/dapp/components/BetaWarning.tsx
+++ b/apps/dapp/components/BetaWarning.tsx
@@ -1,9 +1,7 @@
+import { Message } from '@dao-dao/icons'
+import { Button } from '@dao-dao/ui'
import { ChevronRightIcon, XIcon } from '@heroicons/react/outline'
-import { Button } from '@components'
-
-import SvgMessage from 'components/icons/Message'
-
export function BetaWarningModal({ onAccept }: { onAccept: Function }) {
return (
@@ -50,7 +48,7 @@ export function BetaNotice({ onClose }: { onClose: Function }) {
return (
-
+
DAO DAO is in beta!
diff --git a/apps/dapp/components/Breadcrumbs.tsx b/apps/dapp/components/Breadcrumbs.tsx
deleted file mode 100644
index e0792c63c..000000000
--- a/apps/dapp/components/Breadcrumbs.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import Link from 'next/link'
-
-import { ArrowNarrowLeftIcon } from '@heroicons/react/outline'
-
-/*
- * Breadcrumb style navivation bar. Expects arguments in the form [[link, name], ...].
- */
-export function Breadcrumbs({ crumbs }: { crumbs: Array<[string, string]> }) {
- return (
-
-
-
-
-
-
-
-
- {crumbs.map(([link, name], idx) => (
-
-
- {name}
-
- {idx != crumbs.length - 1 && '/'}
-
- ))}
-
- )
-}
diff --git a/apps/dapp/components/ChainEnableModal.tsx b/apps/dapp/components/ChainEnableModal.tsx
index 5134b1d4c..478823e29 100644
--- a/apps/dapp/components/ChainEnableModal.tsx
+++ b/apps/dapp/components/ChainEnableModal.tsx
@@ -1,7 +1,6 @@
+import { Button, Modal } from '@dao-dao/ui'
import { XIcon } from '@heroicons/react/outline'
-import { Button } from 'ui/Button'
-import { Modal } from './Modal'
const CHAIN_ID = process.env.NEXT_PUBLIC_CHAIN_ID
const CHAIN_NAME = process.env.NEXT_PUBLIC_CHAIN_NAME
diff --git a/apps/dapp/components/Claims.tsx b/apps/dapp/components/Claims.tsx
index 7c09a3931..ace7d03a2 100644
--- a/apps/dapp/components/Claims.tsx
+++ b/apps/dapp/components/Claims.tsx
@@ -1,115 +1,14 @@
-import { MouseEventHandler } from 'react'
-import { useState } from 'react'
-import { useEffect } from 'react'
-
import { useRecoilValue } from 'recoil'
-import { Duration } from '@dao-dao/types/contracts/cw3-dao'
-import { Claim, TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20'
-import { CheckIcon } from '@heroicons/react/outline'
+import { TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20'
+import {
+ ClaimsListItem,
+ ClaimsAvaliableCard as StatelessClaimsAvaliableCard,
+} from '@dao-dao/ui'
+import { claimAvaliable } from '@dao-dao/utils'
import { unstakingDuration } from 'selectors/daos'
import { getBlockHeight, walletClaims } from 'selectors/treasury'
-import {
- convertMicroDenomToDenomWithDecimals,
- humanReadableDuration,
-} from 'util/conversion'
-
-import { LogoNoBorder } from './Logo'
-
-export function claimAvaliable(claim: Claim, blockHeight: number) {
- if ('at_height' in claim.release_at) {
- return blockHeight >= claim.release_at.at_height
- } else if ('at_time' in claim.release_at) {
- const currentTimeNs = new Date().getTime() * 1000000
- return currentTimeNs >= Number(claim.release_at.at_time)
- }
-
- // Unreachable.
- return true
-}
-
-function claimDurationRemaining(claim: Claim, blockHeight: number): Duration {
- if (claimAvaliable(claim, blockHeight)) {
- return { time: 0 }
- }
- if ('at_height' in claim.release_at) {
- const releaseBlock = claim.release_at.at_height
- return { height: releaseBlock - blockHeight }
- } else if ('at_time' in claim.release_at) {
- const currentTimeNs = new Date().getTime() * 1000000
- return {
- time:
- (Number(claim.release_at.at_time) - currentTimeNs) / 1000000000 || 0, // To seconds.
- }
- }
-
- // Unreachable.
- return { time: 0 }
-}
-
-function ClaimListItem({
- claim,
- unstakingDuration,
- blockHeight,
- tokenInfo,
- incrementClaimsAvaliable,
-}: {
- claim: Claim
- unstakingDuration: Duration
- blockHeight: number
- tokenInfo: TokenInfoResponse
- incrementClaimsAvaliable: (n: number) => void
-}) {
- const avaliable = claimAvaliable(claim, blockHeight)
-
- const durationForHumans = humanReadableDuration(unstakingDuration)
- const durationRemaining = claimDurationRemaining(claim, blockHeight)
-
- // Once the claim expires increment claims avaliable.
- useEffect(() => {
- if ('time' in durationRemaining) {
- const id = setTimeout(
- () => incrementClaimsAvaliable(Number(claim.amount)),
- durationRemaining.time * 1000
- )
- return () => clearTimeout(id)
- }
- }, [claim.amount, durationRemaining, incrementClaimsAvaliable])
-
- const [durationRemainingForHumans, setDurationRemainingForHumans] = useState(
- humanReadableDuration(durationRemaining)
- )
-
- useEffect(() => {
- const id = setInterval(() => {
- setDurationRemainingForHumans((_) =>
- humanReadableDuration(claimDurationRemaining(claim, blockHeight))
- )
- }, 1000)
- return () => clearInterval(id)
- }, [claim, blockHeight, setDurationRemainingForHumans])
-
- return (
-
- {avaliable ? (
-
- Avaliable
-
-
- ) : (
-
-
{durationRemainingForHumans || '0'} left
-
/ {durationForHumans}
-
- )}
-
- {convertMicroDenomToDenomWithDecimals(claim.amount, tokenInfo.decimals)}
- ${tokenInfo.symbol}
-
-
- )
-}
export function ClaimsPendingList({
stakingAddress,
@@ -134,7 +33,7 @@ export function ClaimsPendingList({
{claimsPending.map((claim, idx) => {
return (
-
+ onClaim: () => void
loading: boolean
}) {
const blockHeight = useRecoilValue(getBlockHeight)
@@ -168,31 +67,11 @@ export function ClaimAvaliableCard({
.reduce((p, n) => p + Number(n.amount), 0)
return (
-
-
- Unclaimed (unstaked ${tokenInfo.symbol})
-
- {loading ? (
-
-
-
- ) : (
-
- {convertMicroDenomToDenomWithDecimals(
- claimsAvaliable,
- tokenInfo.decimals
- )}
- ${tokenInfo.symbol}
-
- )}
-
-
- Claim
-
-
-
+
)
}
diff --git a/apps/dapp/components/CodeIdSelect.tsx b/apps/dapp/components/CodeIdSelect.tsx
index f29f2f8ee..61e5a7964 100644
--- a/apps/dapp/components/CodeIdSelect.tsx
+++ b/apps/dapp/components/CodeIdSelect.tsx
@@ -1,7 +1,6 @@
+import { Button } from '@dao-dao/ui'
import { ChevronDownIcon } from '@heroicons/react/outline'
-import { Button } from '@components'
-
export interface ContractVersion {
name: string
codeId: number
diff --git a/apps/dapp/components/ConnectWalletButton.tsx b/apps/dapp/components/ConnectWalletButton.tsx
index 634171717..037812a9d 100644
--- a/apps/dapp/components/ConnectWalletButton.tsx
+++ b/apps/dapp/components/ConnectWalletButton.tsx
@@ -1,11 +1,15 @@
-import { useCallback, useState } from 'react'
+import { useCallback } from 'react'
import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil'
-import { CheckCircleIcon, LogoutIcon } from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
-
-import { Button } from '@components'
+import { WalletConnect as StatelessWalletConnect } from '@dao-dao/ui'
+import {
+ CHAIN_ID,
+ NATIVE_DECIMALS,
+ NATIVE_DENOM,
+ convertDenomToHumanReadableDenom,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
import {
connectedWalletAtom,
@@ -18,46 +22,6 @@ import {
noKeplrAccountAtom,
} from 'selectors/cosm'
import { connectKeplrWithoutAlerts } from 'services/keplr'
-import { CHAIN_ID, NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants'
-import {
- convertDenomToHumanReadableDenom,
- convertMicroDenomToDenomWithDecimals,
-} from 'util/conversion'
-
-import SvgCopy from './icons/Copy'
-import SvgWallet from './icons/Wallet'
-
-function CopyButton({ text }: { text: string }) {
- const [copied, setCopied] = useState(false)
- return (
-
- {
- navigator.clipboard.writeText(text)
- setTimeout(() => setCopied(false), 2000)
- setCopied(true)
- }}
- type="button"
- >
- {copied ? (
-
- ) : (
-
- )}
-
-
- )
-}
-
-function DisconnectButton({ onClick }: { onClick: () => void }) {
- return (
-
-
-
-
-
- )
-}
function WalletConnect() {
const [wallet, setWallet] = useRecoilState(connectedWalletAtom)
@@ -80,6 +44,7 @@ function WalletConnect() {
setInstallWarningVisible(true)
} else {
try {
+ console.log(CHAIN_ID)
await connectKeplrWithoutAlerts()
await (window as any).keplr.enable(CHAIN_ID)
setInstallWarningVisible(false)
@@ -106,37 +71,14 @@ function WalletConnect() {
setWallet,
])
- if (walletAddress) {
- return (
-
-
-
-
- {walletName}
-
-
- {walletBalanceHuman} {chainDenomHuman}
-
-
-
-
-
-
-
-
- )
- }
return (
-
+
)
}
diff --git a/apps/dapp/components/ContractCard.tsx b/apps/dapp/components/ContractCard.tsx
index 72cdf77b7..a9421c0ad 100644
--- a/apps/dapp/components/ContractCard.tsx
+++ b/apps/dapp/components/ContractCard.tsx
@@ -2,25 +2,19 @@ import { ReactNode } from 'react'
import Link from 'next/link'
-import { PlusIcon, StarIcon as StarIconOutline } from '@heroicons/react/outline'
-import { StarIcon as StarIconSolid } from '@heroicons/react/solid'
-
-import {
- convertDenomToHumanReadableDenom,
- convertMicroDenomToDenomWithDecimals,
-} from 'util/conversion'
-
+import { Dao, Pencil, Votes } from '@dao-dao/icons'
+import { Logo } from '@dao-dao/ui'
import {
CARD_IMAGES_ENABLED,
NATIVE_DECIMALS,
NATIVE_DENOM,
-} from '../util/constants'
-import SvgDao from './icons/Dao'
-import SvgPencil from './icons/Pencil'
-import SvgVotes from './icons/Votes'
-import { Logo } from './Logo'
+ convertDenomToHumanReadableDenom,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
+import { StarIcon as StarIconOutline } from '@heroicons/react/outline'
+import { StarIcon as StarIconSolid } from '@heroicons/react/solid'
-function DIYLogo({
+function ContractCardBase({
title,
body,
href,
@@ -42,7 +36,7 @@ function DIYLogo({
return (
-
+
@@ -68,7 +62,7 @@ function DIYLogo({
{balance && (
-
+
{convertMicroDenomToDenomWithDecimals(
balance,
NATIVE_DECIMALS
@@ -78,19 +72,13 @@ function DIYLogo({
)}
{proposals != undefined && (
-
+
{proposals} proposal{weight != 1 && 's'}
)}
{weight != undefined && (
-
+
{weight} vote{weight != 1 && 's'}
)}
@@ -124,7 +112,7 @@ export function ContractCard({
}) {
return (
-
)}
-
+
onPin()}
@@ -171,33 +159,3 @@ export function LoadingContractCard() {
)
}
-
-const EmptyStateContractCard = ({
- title,
- description,
- backgroundUrl,
- href,
-}: {
- title: string
- description: string
- backgroundUrl: string
- href: string
-}) => {
- return (
-
-
-
-
-
-
- )
-}
diff --git a/apps/dapp/components/ContractView.tsx b/apps/dapp/components/ContractView.tsx
index 988f66359..712da29b2 100644
--- a/apps/dapp/components/ContractView.tsx
+++ b/apps/dapp/components/ContractView.tsx
@@ -1,17 +1,14 @@
-import { Children, MouseEventHandler, ReactNode } from 'react'
-
import Link from 'next/link'
import { useRecoilValue, waitForAll } from 'recoil'
-import { StarIcon as StarOutline } from '@heroicons/react/outline'
-import { StarIcon as StarSolid } from '@heroicons/react/solid'
-import Tooltip from '@reach/tooltip'
-import { useThemeContext } from 'ui'
-
-import { Button } from '@components'
+import {
+ Button,
+ TreasuryBalances as StatelessTreasuryBalances,
+ Tooltip,
+} from '@dao-dao/ui'
+import { NATIVE_DECIMALS, nativeTokenDecimals } from '@dao-dao/utils'
-import { contractInstantiateTime } from 'selectors/contracts'
import { isMemberSelector } from 'selectors/cosm'
import {
cw20Balances,
@@ -20,238 +17,34 @@ import {
walletAddress,
walletTokenBalanceLoading,
} from 'selectors/treasury'
-import { NATIVE_DECIMALS, HEADER_IMAGES_ENABLED } from 'util/constants'
-import {
- convertDenomToHumanReadableDenom,
- convertMicroDenomToDenomWithDecimals,
- nativeTokenLabel,
- nativeTokenLogoURI,
-} from 'util/conversion'
-import { Logo, LogoNoBorder } from './Logo'
import { ProposalList } from './ProposalList'
-export function GradientHero({ children }: { children: ReactNode }) {
- const theme = useThemeContext()
- const endStop = theme.theme === 'dark' ? '#111213' : '#FFFFFF'
- const baseRgb = theme.accentColor
- ? theme.accentColor.split('(')[1].split(')')[0]
- : '73, 55, 192'
- return (
-
- )
-}
-
-export function StarButton({
- pinned,
- onPin,
-}: {
- pinned: boolean
- onPin: Function
-}) {
- const { accentColor } = useThemeContext()
-
- return (
-
onPin()}
- style={accentColor ? { color: accentColor } : {}}
- >
- {pinned ? (
-
- ) : (
-
- )}
- {pinned ? 'Starred' : 'Star'}
-
- )
-}
-
-export function EstablishedDate({ address }: { address: string }) {
- const instantiateDate = useRecoilValue(contractInstantiateTime(address))
- const formattedDate = instantiateDate.toLocaleDateString(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- })
- return
Est. {formattedDate}
-}
-
-export function HeroContractHeader({
- name,
- address,
- description,
- imgUrl,
-}: {
- name: string
- address: string
- description: string
- imgUrl?: string | null
-}) {
- return (
-
- {imgUrl && HEADER_IMAGES_ENABLED ? (
-
- ) : (
-
- )}
-
-
{name}
-
-
-
-
- )
-}
-
-export function HeroContractHorizontalInfoSection({
- children,
-}: {
- children: ReactNode
-}) {
- return (
-
- {children}
-
- )
-}
-
-export function HeroContractHorizontalInfo({
- children,
-}: {
- children: ReactNode
-}) {
- const childList = Children.toArray(children)
- return (
-
-
- {Children.map(childList, (child) => (
- {child}
- ))}
-
-
- )
-}
-
-export function GovInfoListItem({
- icon,
- text,
- value,
-}: {
- icon: ReactNode
- text: string
- value: string
-}) {
- return (
-
-
- {icon} {text}:
-
- {value}
-
- )
-}
-
-export function BalanceIcon({ iconURI }: { iconURI?: string }) {
- const { accentColor } = useThemeContext()
-
- return (
-
- )
-}
-
-export function BalanceListItem({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- )
-}
-
export function TreasuryBalances({ address }: { address: string }) {
const nativeBalances = useRecoilValue(nativeBalance(address))
- const cw20List = useRecoilValue(cw20Balances(address))
+ const cw20List = useRecoilValue(cw20Balances(address))
const cw20Info = useRecoilValue(
waitForAll(cw20List.map(({ address }) => cw20TokenInfo(address)))
)
- const cw20InfoBalance = cw20Info.map((info, idx) => ({
- info: info,
+
+ const cw20Tokens = cw20Info.map((info, idx) => ({
+ symbol: info.symbol,
amount: cw20List[idx].amount,
+ decimals: info.decimals,
+ }))
+
+ const nativeTokens = nativeBalances.map(({ denom, amount }) => ({
+ denom: denom,
+ amount,
+ decimals: nativeTokenDecimals(denom) || NATIVE_DECIMALS,
}))
return (
-
- {nativeBalances.map((coin, idx) => {
- const symbol = nativeTokenLabel(coin.denom)
- const icon = nativeTokenLogoURI(coin.denom)
- return (
-
-
- {convertMicroDenomToDenomWithDecimals(
- coin.amount,
- NATIVE_DECIMALS
- ).toLocaleString(undefined, {
- maximumFractionDigits: 20,
- })}{' '}
- ${symbol}
-
- )
- })}
- {!nativeBalances.length && (
-
- 0 $
- {convertDenomToHumanReadableDenom(
- process.env.NEXT_PUBLIC_STAKING_DENOM as string
- ).toUpperCase()}
-
- )}
- {cw20InfoBalance.map(({ info, amount }) => {
- return (
-
-
- {convertMicroDenomToDenomWithDecimals(
- amount,
- info.decimals
- ).toLocaleString(undefined, {
- maximumFractionDigits: 20,
- })}{' '}
- ${info.symbol}
-
- )
- })}
-
+
)
}
@@ -295,38 +88,3 @@ export function ContractProposalsDispaly({
>
)
}
-
-export function BalanceCard({
- denom,
- title,
- amount,
- onManage,
- loading,
-}: {
- denom: string
- title: string
- amount: string
- onManage: MouseEventHandler
- loading: boolean
-}) {
- return (
-
-
{title}
- {loading ? (
-
-
-
- ) : (
-
-
- {amount} ${denom}
-
- )}
-
-
- Manage
-
-
-
- )
-}
diff --git a/apps/dapp/components/DaoContractInfo.tsx b/apps/dapp/components/DaoContractInfo.tsx
index dc68eec36..11d1e7a7b 100644
--- a/apps/dapp/components/DaoContractInfo.tsx
+++ b/apps/dapp/components/DaoContractInfo.tsx
@@ -1,5 +1,12 @@
import { useRecoilValue } from 'recoil'
+import { Votes } from '@dao-dao/icons'
+import { CopyToClipboardAccent, GovInfoListItem } from '@dao-dao/ui'
+import {
+ humanReadableDuration,
+ convertMicroDenomToDenomWithDecimals,
+ getThresholdAndQuorumDisplay,
+} from '@dao-dao/utils'
import { CashIcon, ChartPieIcon } from '@heroicons/react/outline'
import {
@@ -7,16 +14,8 @@ import {
tokenConfig,
unstakingDuration as unstakingDurationSelector,
} from 'selectors/daos'
-import {
- humanReadableDuration,
- convertMicroDenomToDenomWithDecimals,
- getThresholdAndQuorumDisplay,
-} from 'util/conversion'
-import { GovInfoListItem } from './ContractView'
-import { CopyToClipboardAccent } from './CopyToClipboard'
import { DaoTreasury } from './DaoTreasury'
-import SvgVotes from './icons/Votes'
export function DaoContractInfo({ address }: { address: string }) {
const daoInfo = useRecoilValue(daoSelector(address))
@@ -43,13 +42,13 @@ export function DaoContractInfo({ address }: { address: string }) {
value={humanReadableDuration(unstakingDuration)}
/>
}
+ icon={ }
text="Passing threshold"
value={threshold}
/>
{quorum && (
}
+ icon={ }
text="Quorum"
value={quorum}
/>
@@ -61,7 +60,7 @@ export function DaoContractInfo({ address }: { address: string }) {
/>
- {' '}
+ {' '}
{convertMicroDenomToDenomWithDecimals(
daoInfo.config.proposal_deposit,
govTokenInfo.decimals
diff --git a/apps/dapp/components/DaoTreasury.tsx b/apps/dapp/components/DaoTreasury.tsx
index 58ce1fd73..a05495635 100644
--- a/apps/dapp/components/DaoTreasury.tsx
+++ b/apps/dapp/components/DaoTreasury.tsx
@@ -4,10 +4,9 @@ import { useRouter } from 'next/router'
import { useRecoilValue } from 'recoil'
+import { Button } from '@dao-dao/ui'
import { PlusSmIcon } from '@heroicons/react/outline'
-import { Button } from '@components'
-
import { daoSelector } from 'selectors/daos'
import { addToken } from 'util/addToken'
diff --git a/apps/dapp/components/HomepageLayout.tsx b/apps/dapp/components/HomepageLayout.tsx
index 7bc39152f..18d3de2d0 100644
--- a/apps/dapp/components/HomepageLayout.tsx
+++ b/apps/dapp/components/HomepageLayout.tsx
@@ -2,7 +2,7 @@ import { ReactNode } from 'react'
import Head from 'next/head'
-import { SITE_TITLE } from 'util/constants'
+import { SITE_TITLE } from '@dao-dao/utils'
export function HomepageLayout({ children }: { children: ReactNode }) {
return (
diff --git a/apps/dapp/components/InstallKeplr.tsx b/apps/dapp/components/InstallKeplr.tsx
index 5883f74e1..0d1a5b44a 100644
--- a/apps/dapp/components/InstallKeplr.tsx
+++ b/apps/dapp/components/InstallKeplr.tsx
@@ -1,7 +1,5 @@
+import { Button, Modal } from '@dao-dao/ui'
import { ChevronRightIcon, XIcon } from '@heroicons/react/outline'
-import { Button } from 'ui/Button'
-
-import { Modal } from './Modal'
export function InstallKeplr({ onClose }: { onClose: () => void }) {
return (
diff --git a/apps/dapp/components/Layout.tsx b/apps/dapp/components/Layout.tsx
index 13e71f693..68f11aa7e 100644
--- a/apps/dapp/components/Layout.tsx
+++ b/apps/dapp/components/Layout.tsx
@@ -8,10 +8,11 @@ import {
useSetRecoilState,
} from 'recoil'
+import { LoadingScreen } from '@dao-dao/ui'
+import { SITE_TITLE } from '@dao-dao/utils'
import { Keplr } from '@keplr-wallet/types'
import { betaWarningAcceptedAtom, showBetaNoticeAtom } from 'atoms/status'
-import LoadingScreen from 'components/LoadingScreen'
import { SidebarLayout } from 'components/SidebarLayout'
import {
kelprOfflineSigner,
@@ -25,7 +26,6 @@ import {
} from 'selectors/cosm'
import { getKeplr, connectKeplrWithoutAlerts } from 'services/keplr'
-import { SITE_TITLE } from '../util/constants'
import { BetaNotice, BetaWarningModal } from './BetaWarning'
import ChainEnableModal from './ChainEnableModal'
import { InstallKeplr } from './InstallKeplr'
diff --git a/apps/dapp/components/MarkdownPreview.tsx b/apps/dapp/components/MarkdownPreview.tsx
deleted file mode 100644
index 30a0e18d7..000000000
--- a/apps/dapp/components/MarkdownPreview.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import ReactMarkdown from 'react-markdown'
-
-export function MarkdownPreview({ markdown }: { markdown: string }) {
- return (
-
- {markdown}
-
- )
-}
diff --git a/apps/dapp/components/Modal.tsx b/apps/dapp/components/Modal.tsx
deleted file mode 100644
index e451a17f9..000000000
--- a/apps/dapp/components/Modal.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ReactNode } from 'react'
-
-export function Modal({ children }: { children: ReactNode }) {
- {
- return (
-
- {children}
-
- )
- }
-}
diff --git a/apps/dapp/components/MultisigContractInfo.tsx b/apps/dapp/components/MultisigContractInfo.tsx
index 603a7c00c..8db20f24a 100644
--- a/apps/dapp/components/MultisigContractInfo.tsx
+++ b/apps/dapp/components/MultisigContractInfo.tsx
@@ -1,14 +1,13 @@
import { useRecoilValue } from 'recoil'
+import { Votes } from '@dao-dao/icons'
+import { CopyToClipboardAccent, GovInfoListItem } from '@dao-dao/ui'
+import { humanReadableDuration, thresholdString } from '@dao-dao/utils'
import { ClockIcon } from '@heroicons/react/outline'
import { sigSelector } from 'selectors/multisigs'
-import { humanReadableDuration, thresholdString } from 'util/conversion'
-import { GovInfoListItem } from './ContractView'
-import { CopyToClipboardAccent } from './CopyToClipboard'
import { DaoTreasury } from './DaoTreasury'
-import SvgVotes from './icons/Votes'
export function MultisigContractInfo({ address }: { address: string }) {
const sigInfo = useRecoilValue(sigSelector(address))
@@ -19,7 +18,7 @@ export function MultisigContractInfo({ address }: { address: string }) {
Governance Details
}
+ icon={ }
text="Passing threshold"
value={thresholdString(sigInfo.config.threshold, true, 0)}
/>
diff --git a/apps/dapp/components/Nav.tsx b/apps/dapp/components/Nav.tsx
index 68ea7a942..6fec18114 100644
--- a/apps/dapp/components/Nav.tsx
+++ b/apps/dapp/components/Nav.tsx
@@ -2,6 +2,8 @@ import Link from 'next/link'
import { useRecoilValue, waitForAll } from 'recoil'
+import { Logo } from '@dao-dao/ui'
+import { SITE_TITLE } from '@dao-dao/utils'
import {
ArrowRightIcon,
ExternalLinkIcon,
@@ -11,12 +13,10 @@ import { MenuIcon } from '@heroicons/react/outline'
import { pinnedDaosAtom, pinnedMultisigsAtom } from 'atoms/pinned'
import { showBetaNoticeAtom } from 'atoms/status'
-import { Logo } from 'components/Logo'
import ThemeToggle from 'components/ThemeToggle'
import { daoSelector } from 'selectors/daos'
import { sigSelector } from 'selectors/multisigs'
-import { SITE_TITLE } from '../util/constants'
import ConnectWalletButton from './ConnectWalletButton'
function MemberDisplay({ name }: { name: string }) {
diff --git a/apps/dapp/components/NoKeplrAccountModal.tsx b/apps/dapp/components/NoKeplrAccountModal.tsx
index 822f0447e..489e27b97 100644
--- a/apps/dapp/components/NoKeplrAccountModal.tsx
+++ b/apps/dapp/components/NoKeplrAccountModal.tsx
@@ -1,7 +1,5 @@
+import { Button, Modal } from '@dao-dao/ui'
import { ChevronRightIcon, XIcon } from '@heroicons/react/outline'
-import { Button } from 'ui/Button'
-
-import { Modal } from './Modal'
export function NoKeplrAccountModal({ onClose }: { onClose: () => void }) {
return (
diff --git a/apps/dapp/components/Paginator.tsx b/apps/dapp/components/Paginator.tsx
index 9937bc6a8..f62302873 100644
--- a/apps/dapp/components/Paginator.tsx
+++ b/apps/dapp/components/Paginator.tsx
@@ -1,6 +1,6 @@
import Link from 'next/link'
-import { Button } from 'ui'
+import { Button } from '@dao-dao/ui'
function Paginator({
page,
diff --git a/apps/dapp/components/ProposalDetails.tsx b/apps/dapp/components/ProposalDetails.tsx
index a21f2b1c7..3d56d76a7 100644
--- a/apps/dapp/components/ProposalDetails.tsx
+++ b/apps/dapp/components/ProposalDetails.tsx
@@ -1,18 +1,20 @@
-import { ReactNode, useState } from 'react'
+import { useState } from 'react'
import { useRouter } from 'next/router'
import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil'
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
-import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao'
-import { EyeIcon, EyeOffIcon } from '@heroicons/react/outline'
+import {
+ StakingMode,
+ ProposalDetails as StatelessProposalDetails,
+ CosmosMessageDisplay,
+} from '@dao-dao/ui'
+import { VoteChoice } from '@dao-dao/ui'
import { FormProvider, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
-import { Button } from 'ui'
import { proposalUpdateCountAtom, proposalsUpdated } from 'atoms/proposals'
-import { MarkdownPreview } from 'components/MarkdownPreview'
import {
cosmWasmSigningClient,
walletAddress as walletAddressSelector,
@@ -26,17 +28,12 @@ import {
import { walletTokenBalanceLoading } from 'selectors/treasury'
import {
FromCosmosMsgProps,
- MessageTemplate,
messageTemplateAndValuesForDecodedCosmosMsg,
} from 'templates/templateList'
import { cleanChainError } from 'util/cleanChainError'
-import { decodedMessagesString, decodeMessages } from 'util/messagehelpers'
import { treasuryTokenListUpdates } from '../atoms/treasury'
-import { CosmosMessageDisplay } from './CosmosMessageDisplay'
-import { Execute } from './Execute'
-import { StakingModal, StakingMode } from './StakingModal'
-import { Vote, VoteChoice } from './Vote'
+import { StakingModal } from './StakingModal'
function executeProposalVote(
choice: VoteChoice,
@@ -124,77 +121,6 @@ function executeProposalExecute(
})
}
-interface ProposalMessageTemplateListItemProps {
- template: MessageTemplate
- values: any
- contractAddress: string
- multisig?: boolean
-}
-
-function ProposalMessageTemplateListItem({
- template,
- values,
- contractAddress,
- multisig,
-}: ProposalMessageTemplateListItemProps) {
- const formMethods = useForm({
- defaultValues: values,
- })
-
- return (
-
-
-
- )
-}
-
-interface ProposalMessageTemplateListProps {
- msgs: CosmosMsgFor_Empty[]
- contractAddress: string
- multisig?: boolean
- fromCosmosMsgProps: FromCosmosMsgProps
-}
-
-function ProposalMessageTemplateList({
- msgs,
- contractAddress,
- multisig,
- fromCosmosMsgProps,
-}: ProposalMessageTemplateListProps) {
- const components: ReactNode[] = msgs.map((msg, index) => {
- const decoded = decodeMessages([msg])[0]
- const data = messageTemplateAndValuesForDecodedCosmosMsg(
- decoded,
- fromCosmosMsgProps
- )
-
- return data ? (
-
- ) : (
- // If no message template found, render raw message.
-
- )
- })
-
- return <>{components}>
-}
-
export function ProposalDetails({
contractAddress,
proposalId,
@@ -251,139 +177,91 @@ export function ProposalDetails({
const weightPercent = (votingPower / totalPower) * 100
const [loading, setLoading] = useState(false)
-
- const [showRaw, setShowRaw] = useState(false)
- const [showStaking, setShowStakng] = useState(false)
+ const [showStaking, setShowStaking] = useState(false)
if (!proposal) {
router.replace(`/${multisig ? 'multisig' : 'dao'}/${contractAddress}`)
return Error
}
- const decodedMessages = decodeMessages(proposal.msgs)
-
return (
-
-
-
{proposal.title}
-
-
-
-
-
Messages
-
- {decodedMessages?.length ? (
- showRaw ? (
-
- ) : (
-
- )
- ) : (
-
[]
- )}
-
- {!!decodedMessages.length && (
-
- setShowRaw((s) => !s)}
- size="sm"
- variant="secondary"
- >
- {showRaw ? (
- <>
- Hide raw data
-
- >
- ) : (
- <>
- Show raw data
-
- >
- )}
-
-
- )}
- {proposal.status === 'passed' && (
- <>
-
Status
-
- executeProposalExecute(
- proposalId,
- contractAddress,
- signingClient,
- wallet,
- () => {
- setProposalUpdates((n) => n + 1)
- setProposalsUpdated((p) =>
- p.includes(proposalId) ? p : p.concat([proposalId])
- )
- setTreasuryTokenListUpdates((n) => n + 1)
- },
- setLoading
- )
- }
- />
- >
- )}
- Vote
- {proposal.status === 'open' && !walletVote && votingPower !== 0 && (
-
- executeProposalVote(
- position,
- proposalId,
- contractAddress,
- signingClient,
- wallet,
- () => {
- setProposalUpdates((n) => n + 1)
- setProposalsUpdated((p) =>
- p.includes(proposalId) ? p : p.concat([proposalId])
- )
- },
- setLoading
+ {
+ const data = messageTemplateAndValuesForDecodedCosmosMsg(
+ message,
+ fromCosmosMsgProps
+ )
+ if (data) {
+ const ThisIsAComponentBecauseReactIsAnnoying = () => {
+ // Can't call `useForm` in a callback.
+ const formMethods = useForm({ defaultValues: data.values })
+ return (
+
+
+
)
}
- voterWeight={weightPercent}
+ return
+ }
+ return (
+
+ )
+ }}
+ onExecute={() =>
+ executeProposalExecute(
+ proposalId,
+ contractAddress,
+ signingClient,
+ wallet,
+ () => {
+ setProposalUpdates((n) => n + 1)
+ setProposalsUpdated((p) =>
+ p.includes(proposalId) ? p : p.concat([proposalId])
+ )
+ setTreasuryTokenListUpdates((n) => n + 1)
+ },
+ setLoading
+ )
+ }
+ onVote={(choice) =>
+ executeProposalVote(
+ choice,
+ proposalId,
+ contractAddress,
+ signingClient,
+ wallet,
+ () => {
+ setProposalUpdates((n) => n + 1)
+ setProposalsUpdated((p) =>
+ p.includes(proposalId) ? p : p.concat([proposalId])
+ )
+ },
+ setLoading
+ )
+ }
+ proposal={proposal}
+ setShowStaking={(s) => setShowStaking(s)}
+ showStaking={showStaking}
+ stakingModal={
+ setTokenBalancesLoading(false)}
+ beforeExecute={() => setTokenBalancesLoading(true)}
+ claimableTokens={0}
+ contractAddress={contractAddress}
+ defaultMode={StakingMode.Stake}
+ onClose={() => setShowStaking(false)}
/>
- )}
- {walletVote && (
- You voted {walletVote} on this proposal.
- )}
- {proposal.status !== 'open' && !walletVote && (
- You did not vote on this proposal.
- )}
- {votingPower === 0 && (
-
- You must have voting power at the time of proposal creation to vote.{' '}
- {!multisig && (
- setShowStakng(true)}>
- Stake some tokens?
-
- )}
- {!multisig && showStaking && (
- setTokenBalancesLoading(false)}
- beforeExecute={() => setTokenBalancesLoading(true)}
- claimAmount={0}
- contractAddress={contractAddress}
- defaultMode={StakingMode.Stake}
- onClose={() => setShowStakng(false)}
- />
- )}
-
- )}
-
+ }
+ walletVote={walletVote}
+ walletWeightPercent={weightPercent}
+ />
)
}
diff --git a/apps/dapp/components/ProposalDetailsSidebar.tsx b/apps/dapp/components/ProposalDetailsSidebar.tsx
index b0a29d9d7..5ae83427a 100644
--- a/apps/dapp/components/ProposalDetailsSidebar.tsx
+++ b/apps/dapp/components/ProposalDetailsSidebar.tsx
@@ -1,7 +1,10 @@
import { useRecoilValue, useRecoilValueLoadable } from 'recoil'
-import { ExternalLinkIcon, CheckIcon, XIcon } from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
+import {
+ ProposalDetailsVoteStatus as StatelessProposalDetailsVoteStatus,
+ ProposalDetailsCard as StatelessProposalDetailsCard,
+ WalletVote as StatelessWalletVote,
+} from '@dao-dao/ui'
import {
proposalSelector,
@@ -10,39 +13,13 @@ import {
walletVoteSelector,
proposalStartBlockSelector,
votingPowerAtHeightSelector,
- WalletVote,
} from 'selectors/proposals'
-import { CHAIN_TXN_URL_PREFIX } from 'util/constants'
import {
contractConfigSelector,
ContractConfigWrapper,
} from 'util/contractConfigWrapper'
-import {
- convertMicroDenomToDenomWithDecimals,
- expirationAtTimeToSecondsFromNow,
- secondsToWdhms,
-} from 'util/conversion'
import { useThresholdQuorum } from 'util/proposal'
-import { CopyToClipboard } from './CopyToClipboard'
-import SvgAbstain from './icons/Abstain'
-import { TriangleUp } from './icons/TriangleUp'
-import { Progress } from './Progress'
-import { ProposalStatus } from './ProposalStatus'
-
-const YouTooltip = ({ label }: { label: string }) => (
-
-
- ?
-
-
-)
-
-const PASSING_THRESHOLD_TOOLTIP =
- "A proposal must attain this proportion of 'Yes' votes to pass."
-const QUORUM_TOOLTIP =
- 'This proportion of voting weight must vote for a proposal to pass.'
-
interface ProposalDetailsProps {
contractAddress: string
multisig: boolean
@@ -51,18 +28,12 @@ interface ProposalDetailsProps {
export const ProposalDetailsCard = ({
contractAddress,
- multisig,
proposalId,
+ multisig,
}: ProposalDetailsProps) => {
const proposal = useRecoilValue(
proposalSelector({ contractAddress, proposalId })
)
- const { state: proposalExecutionTXHashState, contents: txHashContents } =
- useRecoilValueLoadable(
- proposalExecutionTXHashSelector({ contractAddress, proposalId })
- )
- const proposalExecutionTXHash: string | null =
- proposalExecutionTXHashState === 'hasValue' ? txHashContents : null
const walletVote = useRecoilValue(
walletVoteSelector({ contractAddress, proposalId })
@@ -80,115 +51,35 @@ export const ProposalDetailsCard = ({
)
const memberWhenProposalCreated = votingPower > 0
+ const { state: proposalExecutionTXHashState, contents: txHashContents } =
+ useRecoilValueLoadable(
+ proposalExecutionTXHashSelector({ contractAddress, proposalId })
+ )
+ const proposalExecutionTXHash: string | undefined =
+ proposalExecutionTXHashState === 'hasValue' ? txHashContents : undefined
+
if (!proposal) {
- return null
+ return (
+
+
Could not find proposal.
+
+ )
}
return (
-
-
-
-
- Proposal
-
-
-
- # {proposal.id.toString().padStart(6, '0')}
-
-
-
-
-
-
-
-
-
-
-
- You
-
-
- {!memberWhenProposalCreated ? (
-
- ) : walletVote === WalletVote.Yes ? (
-
- Yes
-
- ) : walletVote === WalletVote.No ? (
-
- No
-
- ) : walletVote === WalletVote.Abstain ? (
-
- Abstain
-
- ) : walletVote === WalletVote.Veto ? (
-
- Veto
-
- ) : walletVote ? (
-
- Unknown: {walletVote}
-
- ) : proposal.status === 'open' ? (
-
- ) : (
-
- )}
-
-
-
-
-
Proposer
-
-
-
- {proposal.status === 'executed' &&
- proposalExecutionTXHashState === 'loading' ? (
- <>
-
TX
-
Loading...
- >
- ) : !!proposalExecutionTXHash ? (
- <>
- {CHAIN_TXN_URL_PREFIX ? (
-
- TX
-
-
- ) : (
-
TX
- )}
-
-
- >
- ) : null}
-
-
+
)
}
export const ProposalDetailsVoteStatus = ({
contractAddress,
- multisig,
proposalId,
+ multisig,
}: ProposalDetailsProps) => {
const proposal = useRecoilValue(
proposalSelector({ contractAddress, proposalId })
@@ -210,469 +101,24 @@ export const ProposalDetailsVoteStatus = ({
const configWrapper = new ContractConfigWrapper(config)
const tokenDecimals = configWrapper.gov_token_decimals
- const localeOptions = { maximumSignificantDigits: 3 }
-
- const yesVotes = Number(
- multisig
- ? proposalTally.votes.yes
- : convertMicroDenomToDenomWithDecimals(
- proposalTally.votes.yes,
- tokenDecimals
- )
- )
- const noVotes = Number(
- multisig
- ? proposalTally.votes.no
- : convertMicroDenomToDenomWithDecimals(
- proposalTally.votes.no,
- tokenDecimals
- )
- )
- const abstainVotes = Number(
- multisig
- ? proposalTally.votes.abstain
- : convertMicroDenomToDenomWithDecimals(
- proposalTally.votes.abstain,
- tokenDecimals
- )
- )
-
- const totalWeight = Number(
- multisig
- ? proposalTally.total_weight
- : convertMicroDenomToDenomWithDecimals(
- proposalTally.total_weight,
- tokenDecimals
- )
- )
-
- const turnoutTotal = yesVotes + noVotes + abstainVotes
- const turnoutYesPercent = turnoutTotal ? (yesVotes / turnoutTotal) * 100 : 0
- const turnoutNoPercent = turnoutTotal ? (noVotes / turnoutTotal) * 100 : 0
- const turnoutAbstainPercent = turnoutTotal
- ? (abstainVotes / turnoutTotal) * 100
- : 0
-
- const turnoutPercent = (turnoutTotal / totalWeight) * 100
- const totalYesPercent = (yesVotes / totalWeight) * 100
- const totalNoPercent = (noVotes / totalWeight) * 100
- const totalAbstainPercent = (abstainVotes / totalWeight) * 100
-
- if (!proposal) {
- return null
- }
-
const maxVotingSeconds =
'time' in config.config.max_voting_period
? config.config.max_voting_period.time
: undefined
- const expiresInSeconds =
- proposal.expires && 'at_time' in proposal.expires
- ? expirationAtTimeToSecondsFromNow(proposal.expires)
- : undefined
- // TODO: Change this wen v1 contracts launch, since the conditions
- // will change. In v1, all abstain fails instead of passes.
- const thresholdReached =
- !!threshold &&
- yesVotes >=
- ((quorum ? turnoutTotal : totalWeight) - abstainVotes) *
- (threshold.percent / 100)
- const quorumMet = !!quorum && turnoutPercent >= quorum.percent
-
- const helpfulStatusText =
- proposal.status === 'open' && threshold && quorum
- ? thresholdReached && quorumMet
- ? 'If the current vote stands, this proposal will pass.'
- : !thresholdReached && quorumMet
- ? "If the current vote stands, this proposal will fail because insufficient 'Yes' votes have been cast."
- : thresholdReached && !quorumMet
- ? 'If the current vote stands, this proposal will fail due to a lack of voter participation.'
- : undefined
- : undefined
-
- // When only abstain votes have been cast and there is no quorum,
- // align the abstain progress bar to the right to line up with Abstain
- // text.
- const onlyAbstain = yesVotes === 0 && noVotes === 0 && abstainVotes > 0
+ if (!proposal) {
+ return null
+ }
return (
-
- {helpfulStatusText && (
-
- {helpfulStatusText}
-
- )}
-
- {threshold ? (
- quorum ? (
- <>
-
Ratio of votes
-
-
- {[
-
- Yes{' '}
- {turnoutYesPercent.toLocaleString(undefined, localeOptions)}%
-
,
-
- No {turnoutNoPercent.toLocaleString(undefined, localeOptions)}
- %
-
,
- ]
- .sort(() => yesVotes - noVotes)
- .map((elem, idx) => (
-
- {elem}
-
- ))}
-
- Abstain{' '}
- {turnoutAbstainPercent.toLocaleString(undefined, localeOptions)}
- %
-
-
-
-
-
b.value - a.value),
- {
- value: Number(turnoutAbstainPercent),
- // Secondary is dark with 80% opacity.
- color: 'rgba(var(--dark), 0.8)',
- },
- ],
- },
- ]}
- verticalBars={
- threshold && [
- {
- value: threshold.percent,
- color: 'rgba(var(--dark), 0.5)',
- },
- ]
- }
- />
-
-
-
-
90
- ? 'calc(100% - 32px)'
- : `calc(${threshold.percent}% - 17px)`,
- }}
- width="36px"
- />
-
-
-
-
- Passing threshold:{' '}
- {threshold.display}
-
-
-
- {thresholdReached ? (
- <>
- Passing{' '}
-
- >
- ) : (
- <>
- Failing{' '}
-
- >
- )}
-
-
-
-
-
-
-
- Turnout
-
-
-
- {turnoutPercent.toLocaleString(undefined, localeOptions)}%
-
-
-
-
-
-
-
90
- ? 'calc(100% - 32px)'
- : `calc(${quorum.percent}% - 17px)`,
- }}
- width="36px"
- />
-
-
-
-
- Quorum: {quorum.display}
-
-
-
- {quorumMet ? (
- <>
- Reached{' '}
-
- >
- ) : (
- <>
- Not met{' '}
-
- >
- )}
-
-
-
-
- >
- ) : (
- <>
-
- Turnout
-
-
-
- {[
-
- Yes {totalYesPercent.toLocaleString(undefined, localeOptions)}
- %
-
,
-
- No {totalNoPercent.toLocaleString(undefined, localeOptions)}%
-
,
- ]
- .sort(() => yesVotes - noVotes)
- .map((elem, idx) => (
-
- {elem}
-
- ))}
-
- Abstain{' '}
- {totalAbstainPercent.toLocaleString(undefined, localeOptions)}%
-
-
-
-
-
b.value - a.value),
- {
- value: Number(totalAbstainPercent),
- // Secondary is dark with 80% opacity.
- color: 'rgba(var(--dark), 0.8)',
- },
- ],
- },
- ]}
- verticalBars={[
- {
- value: threshold.percent,
- color: 'rgba(var(--dark), 0.5)',
- },
- ]}
- />
-
-
-
-
90
- ? 'calc(100% - 32px)'
- : `calc(${threshold.percent}% - 17px)`,
- }}
- width="36px"
- />
-
-
-
-
- Passing threshold:{' '}
- {threshold.display}
-
-
-
- {thresholdReached ? (
- <>
- Reached{' '}
-
- >
- ) : (
- <>
- Not met{' '}
-
- >
- )}
-
-
-
-
- >
- )
- ) : null}
-
- {proposal.status === 'open' &&
- expiresInSeconds !== undefined &&
- expiresInSeconds > 0 && (
- <>
-
- Time left
-
-
-
- {secondsToWdhms(expiresInSeconds, 2)}
-
-
- {maxVotingSeconds !== undefined && (
-
- )}
- >
- )}
-
- {threshold?.percent === 50 && yesVotes === noVotes && yesVotes > 0 && (
-
-
Tie clarification
-
-
{"'Yes' will win a tie vote."}
-
- )}
-
- {turnoutTotal > 0 && abstainVotes === turnoutTotal && (
-
-
All abstain clarification
-
-
- {/* TODO: Change this to fail wen v1 contracts. */}
- When all abstain{quorum && ' and the quorum is met'}, a proposal
- will pass.
-
-
- )}
-
+
)
}
diff --git a/apps/dapp/components/ProposalForm.tsx b/apps/dapp/components/ProposalForm.tsx
index 2f8dbbb17..fd52681f4 100644
--- a/apps/dapp/components/ProposalForm.tsx
+++ b/apps/dapp/components/ProposalForm.tsx
@@ -2,11 +2,22 @@ import { useState } from 'react'
import { useRecoilValue } from 'recoil'
+import { Airplane } from '@dao-dao/icons'
import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao'
+import {
+ Button,
+ Tooltip,
+ MarkdownPreview,
+ CosmosMessageDisplay,
+} from '@dao-dao/ui'
+import {
+ InputErrorMessage,
+ InputLabel,
+ TextareaInput,
+ TextInput,
+} from '@dao-dao/ui'
import { EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
import { FormProvider, useFieldArray, useForm } from 'react-hook-form'
-import { Button } from 'ui'
import { walletAddress } from 'selectors/treasury'
import {
@@ -22,13 +33,6 @@ import {
import { validateRequired } from 'util/formValidation'
import { decodedMessagesString } from 'util/messagehelpers'
-import { CosmosMessageDisplay } from './CosmosMessageDisplay'
-import SvgAirplane from './icons/Airplane'
-import { InputErrorMessage } from './input/InputErrorMessage'
-import { InputLabel } from './input/InputLabel'
-import { TextareaInput } from './input/TextAreaInput'
-import { TextInput } from './input/TextInput'
-import { MarkdownPreview } from './MarkdownPreview'
import { ProposalTemplateSelector } from './TemplateSelector'
interface FormProposalData {
@@ -222,7 +226,7 @@ export function ProposalForm({
>
Publish{' '}
-
+
{
return props[0].id
}
-const zeroPad = (num: number, target: number) => {
- const s = num.toString()
- if (s.length > target) {
- return s
- }
- return '0'.repeat(target - s.length) + s
-}
-
-export const getEnd = (exp: Expiration, status: Status) => {
- if (status != 'open' && status != 'pending') {
- return 'Completed'
- }
- if (exp && 'at_time' in exp) {
- const secondsFromNow = expirationAtTimeToSecondsFromNow(exp)
- // Type check, but should never happen.
- if (secondsFromNow === undefined) {
- return ''
- }
-
- if (secondsFromNow <= 0) {
- return 'Completed'
- } else {
- return secondsToWdhms(secondsFromNow)
- }
- }
- // Not much we can say about proposals that expire at a block
- // height / never.
- return ''
-}
-
-function ProposalLine({
- prop,
- contractAddress,
- multisig,
-}: {
- prop: ExtendedProposalResponse
- contractAddress: string
- multisig?: boolean
-}) {
- const proposalKey = prop.draftId ?? prop.id
- const displayKey = prop.draftId ? prop.draftId : zeroPad(prop.id ?? 0, 6)
- return (
-
-
-
-
-
{prop.title}
-
{getEnd(prop.expires, prop.status)}
-
-
-
- )
-}
-
export function ProposalList({
contractAddress,
multisig,
@@ -121,12 +42,6 @@ export function ProposalList({
const [startBefore, setStartBefore] = useRecoilState(
proposalsRequestStartBeforeAtom
)
- const draftProposals = useRecoilValue(draftProposalsSelector(contractAddress))
- // const propList = useRecoilValue(proposalsSelector({
- // contractAddress,
- // startBefore,
- // limit: 10
- // }))
// The proposals that we have loaded.
const [propList, setPropList] = useRecoilState(
proposalListAtom(contractAddress)
@@ -139,7 +54,7 @@ export function ProposalList({
// Update the proposal list with any proposals that were created
// since we were last here.
const newProps = useRecoilValue(
- onChainProposalsSelector({
+ proposalsSelector({
contractAddress,
startBefore: getNewestLoadedProposal(propList) + propsCreated + 1,
limit: propsCreated,
@@ -163,7 +78,7 @@ export function ProposalList({
// Update the proposal list with any proposals that have been
// requested by a load more press or first load of this page.
const existingProps = useRecoilValue(
- onChainProposalsSelector({
+ proposalsSelector({
contractAddress,
startBefore,
limit: PROP_LOAD_LIMIT,
@@ -220,41 +135,40 @@ export function ProposalList({
const proposalsTotal = useRecoilValue(proposalCount(contractAddress))
const showLoadMore = propList.length < proposalsTotal
- const allProposals = (
- draftProposalsToExtendedResponses(draftProposals) ?? []
- ).concat(propList)
-
- if (!allProposals.length) {
+ if (!propList.length) {
return no proposals
}
return (
- {allProposals.map((prop, idx) => {
- const key = `prop_${prop.draftId ?? prop.id ?? idx}`
+ {propList.map((proposal) => {
+ const key = `prop_${proposal.id}`
return (
)
})}
{showLoadMore && (
-
{
const proposal = propList && propList[propList.length - 1]
if (proposal) {
setStartBefore(proposal.id)
}
}}
+ size="sm"
+ variant="secondary"
>
Load more
-
+
)}
)
diff --git a/apps/dapp/components/SidebarLayout.tsx b/apps/dapp/components/SidebarLayout.tsx
index a5e2a3436..0b094364b 100644
--- a/apps/dapp/components/SidebarLayout.tsx
+++ b/apps/dapp/components/SidebarLayout.tsx
@@ -3,10 +3,10 @@ import { useState } from 'react'
import Link from 'next/link'
+import { Logo } from '@dao-dao/ui'
+import { SITE_TITLE } from '@dao-dao/utils'
import { MenuIcon } from '@heroicons/react/outline'
-import { SITE_TITLE } from '../util/constants'
-import { Logo } from './Logo'
import Nav from './Nav'
const SmallScreenNav = ({ onMenuClick }: { onMenuClick: () => void }) => {
diff --git a/apps/dapp/components/StakingModal.tsx b/apps/dapp/components/StakingModal.tsx
index 150ea5e26..cb9c246ae 100644
--- a/apps/dapp/components/StakingModal.tsx
+++ b/apps/dapp/components/StakingModal.tsx
@@ -1,187 +1,35 @@
-import {
- ChangeEventHandler,
- Dispatch,
- MouseEventHandler,
- ReactNode,
- SetStateAction,
- useState,
-} from 'react'
+import { useState } from 'react'
import { SetterOrUpdater, useRecoilValue, useSetRecoilState } from 'recoil'
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { TokenInfoResponse } from '@dao-dao/types/contracts/cw20-gov'
-import { Duration } from '@dao-dao/types/contracts/cw3-dao'
+import { StakingMode, StakingModal as StatelessStakingModal } from '@dao-dao/ui'
import {
- ChevronLeftIcon,
- ChevronRightIcon,
- XIcon,
-} from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
+ convertDenomToMicroDenomWithDecimals,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
import toast from 'react-hot-toast'
-import { Button } from 'ui'
import {
cosmWasmSigningClient,
walletAddress as walletAddressSelector,
} from 'selectors/cosm'
-import { daoSelector, unstakingDuration } from 'selectors/daos'
+import {
+ daoSelector,
+ unstakingDuration as unstakingDurationSelector,
+} from 'selectors/daos'
import {
cw20TokenInfo,
walletStakedTokenBalance,
walletTokenBalance,
+ walletTokenBalanceLoading,
walletTokenBalanceUpdateCountAtom,
} from 'selectors/treasury'
import { cleanChainError } from 'util/cleanChainError'
-import {
- convertDenomToMicroDenomWithDecimals,
- convertMicroDenomToDenomWithDecimals,
- humanReadableDuration,
-} from 'util/conversion'
-
-import { Modal } from './Modal'
-
-export enum StakingMode {
- Stake,
- Unstake,
- Claim,
-}
-
-function stakingModeString(mode: StakingMode) {
- switch (mode) {
- case StakingMode.Stake:
- return 'stake'
- case StakingMode.Unstake:
- return 'unstake'
- case StakingMode.Claim:
- return 'claim'
- default:
- return 'internal error'
- }
-}
-
-function ModeButton({
- onClick,
- active,
- children,
-}: {
- onClick: MouseEventHandler
- active: boolean
- children: ReactNode
-}) {
- return (
-
- {children}
-
- )
-}
-
-function AmountSelector({
- onIncrease,
- onDecrease,
- onChange,
- amount,
- max,
-}: {
- onIncrease: MouseEventHandler
- onDecrease: MouseEventHandler
- onChange: ChangeEventHandler
- amount: string
- max: number
-}) {
- return (
-
-
-
-
-
- = max
- ? 'bg-transparent border border-inactive'
- : ''
- }`}
- disabled={Number(amount) + 1 >= max}
- onClick={onIncrease}
- >
-
-
-
- )
-}
-
-function PercentSelector({
- max,
- amount,
- setAmount,
-}: {
- max: number
- amount: number
- setAmount: Dispatch>
-}) {
- const active = (p: number) => max * p == amount
- const getClassName = (p: number) =>
- 'rounded-md transition hover:bg-secondary link-text font-normal px-2 py-1' +
- (active(p) ? ' bg-secondary border border-inactive' : '')
- const getOnClick = (p: number) => () => {
- setAmount(
- (p * max)
- // Need to specify 'en' here or otherwise different langauges
- // (ex german) will insert '.' as seperators which will mess
- // with our replace logic :)
- .toLocaleString('en', { maximumFractionDigits: 6 })
- .replaceAll(',', '')
- )
- }
-
- return (
-
-
- 10%
-
-
- 25%
-
-
- 50%
-
-
- 75%
-
-
- 100%
-
-
- )
-}
-
-function durationIsNonZero(d: Duration) {
- if ('height' in d) {
- return d.height !== 0
- }
- return d.time !== 0
-}
function executeUnstakeAction(
- denomAmount: string,
+ denomAmount: number,
tokenInfo: TokenInfoResponse,
stakingAddress: string,
signingClient: SigningCosmWasmClient | null,
@@ -192,10 +40,12 @@ function executeUnstakeAction(
if (!signingClient) {
toast.error('Please connect your wallet')
}
+
const amount = convertDenomToMicroDenomWithDecimals(
denomAmount,
tokenInfo.decimals
)
+
setLoading(true)
signingClient
?.execute(
@@ -219,7 +69,7 @@ function executeUnstakeAction(
}
function executeStakeAction(
- denomAmount: string,
+ denomAmount: number,
tokenAddress: string,
tokenInfo: TokenInfoResponse,
stakingAddress: string,
@@ -295,20 +145,19 @@ function executeClaimAction(
export function StakingModal({
defaultMode,
contractAddress,
- claimAmount,
+ claimableTokens,
onClose,
beforeExecute,
afterExecute,
}: {
defaultMode: StakingMode
contractAddress: string
- claimAmount: number
- onClose: MouseEventHandler
+ claimableTokens: number
+ onClose: () => void
beforeExecute: Function
afterExecute: Function
}) {
- const [mode, setMode] = useState(defaultMode)
- const [amount, setAmount] = useState('0')
+ const [amount, setAmount] = useState(0)
const walletAddress = useRecoilValue(walletAddressSelector)
const signingClient = useRecoilValue(cosmWasmSigningClient)
@@ -320,215 +169,105 @@ export function StakingModal({
useRecoilValue(walletTokenBalance(daoInfo?.gov_token)).amount,
tokenInfo.decimals
)
+ const tokenBalanceLoading = useRecoilValue(
+ walletTokenBalanceLoading(walletAddress)
+ )
const stakedGovTokenBalance = convertMicroDenomToDenomWithDecimals(
useRecoilValue(walletStakedTokenBalance(daoInfo?.staking_contract)).amount,
tokenInfo.decimals
)
- const unstakeDuration = useRecoilValue(
- unstakingDuration(daoInfo.staking_contract)
+ const unstakingDuration = useRecoilValue(
+ unstakingDurationSelector(daoInfo.staking_contract)
)
const setWalletTokenBalanceUpdateCount = useSetRecoilState(
walletTokenBalanceUpdateCountAtom(walletAddress)
)
- const maxTx = Number(
- mode === StakingMode.Stake ? govTokenBalance : stakedGovTokenBalance
- )
- const canClaim = claimAmount !== 0
-
- const invalidAmount = (): string => {
- if (amount === '') {
- return 'Unspecified amount'
- }
- if (Number(amount) > maxTx || Number(amount) <= 0) {
- return `Invalid amount`
- }
- return ''
- }
- const walletDisconnected = (): string => {
+ const walletDisconnected = (): string | undefined => {
if (!signingClient || !walletAddress) {
return 'Please connect your wallet'
}
- return ''
+ return undefined
}
- const error = invalidAmount() || walletDisconnected()
- const ready = !error
+ const error = walletDisconnected()
- const ActionButton = ({ ready }: { ready: boolean }) => {
- return (
- {
- beforeExecute()
- if (mode === StakingMode.Stake) {
- executeStakeAction(
- amount,
- daoInfo.gov_token,
- tokenInfo,
- daoInfo.staking_contract,
- signingClient,
- walletAddress,
- setLoading,
- () => {
- setAmount('0')
- // New staking balances will not appear until the next block has been added.
- setTimeout(() => {
- setWalletTokenBalanceUpdateCount((p) => p + 1)
- afterExecute()
- }, 6000)
- }
- )
- } else if (mode === StakingMode.Unstake) {
- executeUnstakeAction(
- amount,
- tokenInfo,
- daoInfo.staking_contract,
- signingClient,
- walletAddress,
- setLoading,
- () => {
- setAmount('0')
- // New staking balances will not appear until the next block has been added.
- setTimeout(() => {
- setWalletTokenBalanceUpdateCount((p) => p + 1)
- afterExecute()
- }, 6500)
- }
- )
- } else if (mode === StakingMode.Claim) {
- executeClaimAction(
- convertMicroDenomToDenomWithDecimals(
- claimAmount,
- tokenInfo.decimals
- ),
- daoInfo.staking_contract,
- signingClient,
- walletAddress,
- setLoading,
- () => {
- setTimeout(() => {
- setWalletTokenBalanceUpdateCount((p) => p + 1)
- afterExecute()
- }, 6500)
- }
- )
+ const onAction = (mode: StakingMode, amount: number) => {
+ beforeExecute()
+ switch (mode) {
+ case StakingMode.Stake:
+ executeStakeAction(
+ amount,
+ daoInfo.gov_token,
+ tokenInfo,
+ daoInfo.staking_contract,
+ signingClient,
+ walletAddress,
+ setLoading,
+ () => {
+ setAmount(0)
+ // New staking balances will not appear until the next block has been added.
+ setTimeout(() => {
+ setWalletTokenBalanceUpdateCount((p) => p + 1)
+ afterExecute()
+ }, 6000)
}
- }}
- >
- {stakingModeString(mode)}
-
- )
+ )
+ break
+ case StakingMode.Unstake:
+ executeUnstakeAction(
+ amount,
+ tokenInfo,
+ daoInfo.staking_contract,
+ signingClient,
+ walletAddress,
+ setLoading,
+ () => {
+ setAmount(0)
+ // New staking balances will not appear until the next block has been added.
+ setTimeout(() => {
+ setWalletTokenBalanceUpdateCount((p) => p + 1)
+ afterExecute()
+ }, 6500)
+ }
+ )
+ break
+ case StakingMode.Claim:
+ executeClaimAction(
+ convertMicroDenomToDenomWithDecimals(amount, tokenInfo.decimals),
+ daoInfo.staking_contract,
+ signingClient,
+ walletAddress,
+ setLoading,
+ () => {
+ setTimeout(() => {
+ setWalletTokenBalanceUpdateCount((p) => p + 1)
+ afterExecute()
+ }, 6500)
+ }
+ )
+ break
+ default:
+ toast.error('Internal error while staking. Unrecognized mode.')
+ }
}
return (
-
-
-
-
-
-
-
-
Manage staking
-
-
- setMode(StakingMode.Stake)}
- >
- Stake
-
- setMode(StakingMode.Unstake)}
- >
- Unstake
-
- {canClaim && (
- setMode(StakingMode.Claim)}
- >
- Claim
-
- )}
-
- {mode !== StakingMode.Claim && (
- <>
-
-
Choose your token amount
-
setAmount(e?.target?.value)}
- onDecrease={() => setAmount((a) => (Number(a) - 1).toString())}
- onIncrease={() => setAmount((a) => (Number(a) + 1).toString())}
- />
- {Number(amount) > maxTx && (
-
- Can{"'"}t {stakingModeString(mode)} more ${tokenInfo.symbol}{' '}
- than you own
-
- )}
-
- Max available {maxTx} ${tokenInfo.symbol}
-
-
-
- {mode === StakingMode.Unstake &&
- durationIsNonZero(unstakeDuration) && (
- <>
-
-
-
- Unstaking period: {humanReadableDuration(unstakeDuration)}
-
-
- There will be {humanReadableDuration(unstakeDuration)}{' '}
- between the time you decide to unstake your tokens and the
- time you can redeem them.
-
-
- >
- )}
-
- >
- )}
- {mode === StakingMode.Claim && (
- <>
-
-
- {convertMicroDenomToDenomWithDecimals(
- claimAmount,
- tokenInfo.decimals
- )}{' '}
- ${tokenInfo.symbol} avaliable
-
-
- Claim them to increase your voting power.
-
-
-
- >
- )}
-
-
+ setAmount(newAmount)}
+ stakableTokens={govTokenBalance}
+ tokenDecimals={tokenInfo.decimals}
+ tokenSymbol={tokenInfo.symbol}
+ unstakableTokens={stakedGovTokenBalance}
+ unstakingDuration={unstakingDuration}
+ />
)
}
diff --git a/apps/dapp/components/StatusIcons/StatusIcons.tsx b/apps/dapp/components/StatusIcons/StatusIcons.tsx
deleted file mode 100644
index 88e1ed377..000000000
--- a/apps/dapp/components/StatusIcons/StatusIcons.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import SvgDraft from '../icons/Draft'
-import SvgExecuted from '../icons/Executed'
-import SvgOpen from '../icons/Open'
-import SvgPassed from '../icons/Passed'
-import SvgRejected from '../icons/Rejected'
-
-export const StatusIcons: { [key: string]: JSX.Element } = {
- draft: ,
- open: ,
- executed: ,
- passed: ,
- rejected: ,
-}
diff --git a/apps/dapp/components/TemplateSelector.tsx b/apps/dapp/components/TemplateSelector.tsx
index acfb00e6d..45495971b 100644
--- a/apps/dapp/components/TemplateSelector.tsx
+++ b/apps/dapp/components/TemplateSelector.tsx
@@ -1,10 +1,9 @@
+import { Modal } from '@dao-dao/ui'
import { XIcon } from '@heroicons/react/outline'
import { ContractSupport, MessageTemplate } from 'templates/templateList'
import { Config } from 'util/contractConfigWrapper'
-import { Modal } from './Modal'
-
export function MessageTemplateDisplayItem({
template,
onClick,
diff --git a/apps/dapp/components/ThemeToggle.tsx b/apps/dapp/components/ThemeToggle.tsx
index 74ce42149..42b958e90 100644
--- a/apps/dapp/components/ThemeToggle.tsx
+++ b/apps/dapp/components/ThemeToggle.tsx
@@ -1,5 +1,5 @@
+import { useThemeContext } from '@dao-dao/ui'
import { MoonIcon, SunIcon } from '@heroicons/react/outline'
-import { useThemeContext } from 'ui'
export const defaultTheme = 'dark'
diff --git a/apps/dapp/components/Transfers.tsx b/apps/dapp/components/Transfers.tsx
index 994c0a6c1..4187dfebe 100644
--- a/apps/dapp/components/Transfers.tsx
+++ b/apps/dapp/components/Transfers.tsx
@@ -1,13 +1,13 @@
import { useRecoilValueLoadable } from 'recoil'
import { IndexedTx } from '@cosmjs/stargate'
-
-import { transactions } from 'selectors/treasury'
-import { NATIVE_DECIMALS } from 'util/constants'
import {
convertMicroDenomToDenomWithDecimals,
nativeTokenLabel,
-} from 'util/conversion'
+ NATIVE_DECIMALS,
+} from '@dao-dao/utils'
+
+import { transactions } from 'selectors/treasury'
interface TxEventAttribute {
key: string
diff --git a/apps/dapp/components/icons/Abstain.tsx b/apps/dapp/components/icons/Abstain.tsx
deleted file mode 100644
index 0c62b8834..000000000
--- a/apps/dapp/components/icons/Abstain.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgAbstain = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgAbstain
diff --git a/apps/dapp/components/icons/Airplane.tsx b/apps/dapp/components/icons/Airplane.tsx
deleted file mode 100644
index be081ede9..000000000
--- a/apps/dapp/components/icons/Airplane.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgAirplane = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgAirplane
diff --git a/apps/dapp/components/icons/ArrowUpRight.tsx b/apps/dapp/components/icons/ArrowUpRight.tsx
deleted file mode 100644
index fc45ef151..000000000
--- a/apps/dapp/components/icons/ArrowUpRight.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgArrowUpRight = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgArrowUpRight
diff --git a/apps/dapp/components/icons/Connect.tsx b/apps/dapp/components/icons/Connect.tsx
deleted file mode 100644
index 508a52cf1..000000000
--- a/apps/dapp/components/icons/Connect.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgConnect = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgConnect
diff --git a/apps/dapp/components/icons/Copy.tsx b/apps/dapp/components/icons/Copy.tsx
deleted file mode 100644
index 4bbafa15c..000000000
--- a/apps/dapp/components/icons/Copy.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgCopy = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgCopy
diff --git a/apps/dapp/components/icons/Dao.tsx b/apps/dapp/components/icons/Dao.tsx
deleted file mode 100644
index 2f153afeb..000000000
--- a/apps/dapp/components/icons/Dao.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgDao = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgDao
diff --git a/apps/dapp/components/icons/Discord.tsx b/apps/dapp/components/icons/Discord.tsx
deleted file mode 100644
index 047430809..000000000
--- a/apps/dapp/components/icons/Discord.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgDiscord = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgDiscord
diff --git a/apps/dapp/components/icons/Draft.tsx b/apps/dapp/components/icons/Draft.tsx
deleted file mode 100644
index ce0dad7e7..000000000
--- a/apps/dapp/components/icons/Draft.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgDraft = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgDraft
diff --git a/apps/dapp/components/icons/Executed.tsx b/apps/dapp/components/icons/Executed.tsx
deleted file mode 100644
index 549b71e32..000000000
--- a/apps/dapp/components/icons/Executed.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgExecuted = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgExecuted
diff --git a/apps/dapp/components/icons/Github.tsx b/apps/dapp/components/icons/Github.tsx
deleted file mode 100644
index a4ce2069c..000000000
--- a/apps/dapp/components/icons/Github.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgGithub = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgGithub
diff --git a/apps/dapp/components/icons/MemberCheck.tsx b/apps/dapp/components/icons/MemberCheck.tsx
deleted file mode 100644
index fad6c2662..000000000
--- a/apps/dapp/components/icons/MemberCheck.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgMemberCheck = (props: SVGProps) => (
-
-
-
-
-)
-
-export default SvgMemberCheck
diff --git a/apps/dapp/components/icons/Message.tsx b/apps/dapp/components/icons/Message.tsx
deleted file mode 100644
index 4c07e55a1..000000000
--- a/apps/dapp/components/icons/Message.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgMessage = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgMessage
diff --git a/apps/dapp/components/icons/Open.tsx b/apps/dapp/components/icons/Open.tsx
deleted file mode 100644
index 0cc20a9e8..000000000
--- a/apps/dapp/components/icons/Open.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgOpen = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgOpen
diff --git a/apps/dapp/components/icons/Passed.tsx b/apps/dapp/components/icons/Passed.tsx
deleted file mode 100644
index 029b19669..000000000
--- a/apps/dapp/components/icons/Passed.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgPassed = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgPassed
diff --git a/apps/dapp/components/icons/Pencil.tsx b/apps/dapp/components/icons/Pencil.tsx
deleted file mode 100644
index 868be97d4..000000000
--- a/apps/dapp/components/icons/Pencil.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgPencil = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgPencil
diff --git a/apps/dapp/components/icons/Rejected.tsx b/apps/dapp/components/icons/Rejected.tsx
deleted file mode 100644
index 4e23a67ee..000000000
--- a/apps/dapp/components/icons/Rejected.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgRejected = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgRejected
diff --git a/apps/dapp/components/icons/Twitter.tsx b/apps/dapp/components/icons/Twitter.tsx
deleted file mode 100644
index c759bb35f..000000000
--- a/apps/dapp/components/icons/Twitter.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgTwitter = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgTwitter
diff --git a/apps/dapp/components/icons/Votes.tsx b/apps/dapp/components/icons/Votes.tsx
deleted file mode 100644
index cb0c7918d..000000000
--- a/apps/dapp/components/icons/Votes.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgVotes = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgVotes
diff --git a/apps/dapp/components/icons/Wallet.tsx b/apps/dapp/components/icons/Wallet.tsx
deleted file mode 100644
index 4eeaaffa7..000000000
--- a/apps/dapp/components/icons/Wallet.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-const SvgWallet = (props: SVGProps) => (
-
-
-
-)
-
-export default SvgWallet
diff --git a/apps/dapp/components/index.ts b/apps/dapp/components/index.ts
deleted file mode 100644
index 80d03e816..000000000
--- a/apps/dapp/components/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { Button } from 'ui/Button'
-export { ProposalStatus } from './ProposalStatus'
-export { StatusIcons } from './StatusIcons'
diff --git a/apps/dapp/models/proposal/proposal.ts b/apps/dapp/models/proposal/proposal.ts
index 881b39bbd..b1b23a316 100644
--- a/apps/dapp/models/proposal/proposal.ts
+++ b/apps/dapp/models/proposal/proposal.ts
@@ -9,10 +9,6 @@ import {
Votes,
} from '@dao-dao/types/contracts/cw3-dao'
-import { ProposalMapItem } from 'types/proposals'
-
-import { labelForMessage } from '../../util/messagehelpers'
-
export const MEMO_MAX_LEN = 255
const EmptyThreshold: Threshold = {
@@ -45,12 +41,6 @@ export const EmptyProposal: Proposal = {
votes: { ...EmptyVotes },
}
-export const EmptyProposalItem: ProposalMapItem = {
- proposal: EmptyProposal,
- id: '',
- draft: true,
-}
-
export const EmptyThresholdResponse: ThresholdResponse = {
absolute_percentage: {
percentage: '0',
@@ -76,13 +66,3 @@ export const EmptyProposalResponse: ProposalResponse = {
threshold: { ...EmptyThresholdResponse },
total_weight: '0',
}
-
-export function memoForProposal(proposal: Proposal): string {
- const messagesMemo = proposal.msgs
- ? proposal.msgs.map((msg) => labelForMessage(msg)).join(', ')
- : ''
- return `${proposal.title}\n${proposal.description}\n\n${messagesMemo}`.slice(
- 0,
- MEMO_MAX_LEN
- )
-}
diff --git a/apps/dapp/next.config.js b/apps/dapp/next.config.js
index d9e81317d..9556fc36d 100644
--- a/apps/dapp/next.config.js
+++ b/apps/dapp/next.config.js
@@ -2,7 +2,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
const withTM = require('next-transpile-modules')([
- 'ui',
+ '@dao-dao/ui',
'@dao-dao/icons',
'@dao-dao/utils',
])
diff --git a/apps/dapp/package.json b/apps/dapp/package.json
index 453ecb6e2..7fd2fb517 100644
--- a/apps/dapp/package.json
+++ b/apps/dapp/package.json
@@ -25,6 +25,7 @@
"@dao-dao/icons": "*",
"@dao-dao/types": "^0.0.8",
"@dao-dao/utils": "*",
+ "@dao-dao/ui": "*",
"@headlessui/react": "^1.4.2",
"@heroicons/react": "^1.0.5",
"@keplr-wallet/types": "^0.9.10",
@@ -53,8 +54,7 @@
"react-markdown": "^8.0.0",
"recoil": "^0.5.2",
"rich-markdown-editor": "^11.21.3",
- "styled-components": "^5.3.3",
- "ui": "*"
+ "styled-components": "^5.3.3"
},
"devDependencies": {
"@babel/core": "^7.16.7",
diff --git a/apps/dapp/pages/_app.tsx b/apps/dapp/pages/_app.tsx
index acc42ec58..a55485092 100644
--- a/apps/dapp/pages/_app.tsx
+++ b/apps/dapp/pages/_app.tsx
@@ -1,4 +1,4 @@
-import 'ui/globals.css'
+import '@dao-dao/ui/globals.css'
import 'styles/app.css'
import { useState, useEffect } from 'react'
import { Suspense } from 'react'
@@ -8,12 +8,11 @@ import { useRouter } from 'next/router'
import { RecoilRoot } from 'recoil'
-import { DEFAULT_THEME_NAME, ThemeProvider } from 'ui'
+import { DEFAULT_THEME_NAME, ThemeProvider, LoadingScreen } from '@dao-dao/ui'
import ErrorBoundary from 'components/ErrorBoundary'
import { HomepageLayout } from 'components/HomepageLayout'
import SidebarLayout from 'components/Layout'
-import LoadingScreen from 'components/LoadingScreen'
import Notifications from 'components/Notifications'
function MyApp({ Component, pageProps }: AppProps) {
diff --git a/apps/dapp/pages/_document.tsx b/apps/dapp/pages/_document.tsx
index aa5248b45..f50ae8ead 100644
--- a/apps/dapp/pages/_document.tsx
+++ b/apps/dapp/pages/_document.tsx
@@ -2,7 +2,7 @@ import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
-import { SITE_TITLE } from 'util/constants'
+import { SITE_TITLE } from '@dao-dao/utils'
class MyDocument extends Document {
static async getInitialProps(ctx: any) {
diff --git a/apps/dapp/pages/dao/[contractAddress]/index.tsx b/apps/dapp/pages/dao/[contractAddress]/index.tsx
index 5e0d28035..8cbca66ce 100644
--- a/apps/dapp/pages/dao/[contractAddress]/index.tsx
+++ b/apps/dapp/pages/dao/[contractAddress]/index.tsx
@@ -3,28 +3,33 @@ import React, { useEffect, useState } from 'react'
import type { GetStaticPaths, GetStaticProps, NextPage } from 'next'
import { useRouter } from 'next/router'
-import { useRecoilState, useRecoilValue } from 'recoil'
+import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
+import { MemberCheck, Pencil } from '@dao-dao/icons'
+import {
+ useThemeContext,
+ StakingMode,
+ GradientHero,
+ HorizontalInfo,
+ ContractHeader,
+ StarButton,
+ HorizontalInfoSection,
+ BalanceCard,
+ Breadcrumbs,
+} from '@dao-dao/ui'
+import {
+ convertMicroDenomToDenomWithDecimals,
+ claimAvaliable,
+} from '@dao-dao/utils'
import { LibraryIcon, PlusSmIcon, UsersIcon } from '@heroicons/react/outline'
-import { useThemeContext } from 'ui'
-import { claimAvaliable, ClaimsPendingList } from '@components/Claims'
+import { ClaimsPendingList } from '@components/Claims'
import { DaoContractInfo } from '@components/DaoContractInfo'
-import SvgMemberCheck from '@components/icons/MemberCheck'
-import SvgPencil from '@components/icons/Pencil'
import { pinnedDaosAtom } from 'atoms/pinned'
-import { Breadcrumbs } from 'components/Breadcrumbs'
-import {
- BalanceCard,
- ContractProposalsDispaly,
- GradientHero,
- HeroContractHorizontalInfo,
- HeroContractHeader,
- StarButton,
- HeroContractHorizontalInfoSection,
-} from 'components/ContractView'
+import { ContractProposalsDispaly } from 'components/ContractView'
import ErrorBoundary from 'components/ErrorBoundary'
-import { StakingModal, StakingMode } from 'components/StakingModal'
+import { StakingModal } from 'components/StakingModal'
+import { contractInstantiateTime } from 'selectors/contracts'
import { CHAIN_RPC_ENDPOINT, isMemberSelector } from 'selectors/cosm'
import {
daoSelector,
@@ -39,11 +44,11 @@ import {
walletStakedTokenBalance,
walletTokenBalance,
walletTokenBalanceLoading,
+ walletTokenBalanceUpdateCountAtom,
} from 'selectors/treasury'
import { addToken } from 'util/addToken'
import { cosmWasmClientRouter } from 'util/chainClientRouter'
import { getFastAverageColor } from 'util/colors'
-import { convertMicroDenomToDenomWithDecimals } from 'util/conversion'
function DaoHome() {
const router = useRouter()
@@ -51,6 +56,9 @@ function DaoHome() {
const daoInfo = useRecoilValue(daoSelector(contractAddress))
const tokenInfo = useRecoilValue(tokenConfig(daoInfo?.gov_token))
+ const establishedDate = useRecoilValue(
+ contractInstantiateTime(contractAddress)
+ )
const stakedTotal = useRecoilValue(totalStaked(daoInfo?.staking_contract))
const proposalsTotal = useRecoilValue(proposalCount(contractAddress))
const { member } = useRecoilValue(isMemberSelector(contractAddress))
@@ -62,18 +70,17 @@ function DaoHome() {
)
const blockHeight = useRecoilValue(getBlockHeight)
const stuff = useRecoilValue(walletClaims(daoInfo.staking_contract))
- const initialClaimsAvaliable = stuff.claims
+ const claimsAvaliable = stuff.claims
.filter((c) => claimAvaliable(c, blockHeight))
.reduce((p, n) => p + Number(n.amount), 0)
- // If a claim becomes avaliable while the page is open we need a way to update
- // the number of claims avaliable.
- const [claimsAvaliable, setClaimsAvaliable] = useState(initialClaimsAvaliable)
-
const wallet = useRecoilValue(walletAddress)
const [tokenBalanceLoading, setTokenBalancesLoading] = useRecoilState(
walletTokenBalanceLoading(wallet)
)
+ const setWalletTokenBalanceUpdateCount = useSetRecoilState(
+ walletTokenBalanceUpdateCountAtom(wallet)
+ )
const [showStaking, setShowStaking] = useState(false)
@@ -106,7 +113,7 @@ function DaoHome() {
{member && (
)}
@@ -124,32 +131,32 @@ function DaoHome() {
-
-
-
+
+
{convertMicroDenomToDenomWithDecimals(
tokenInfo.total_supply,
tokenInfo.decimals
).toLocaleString()}{' '}
${tokenInfo?.symbol} total supply
-
-
+
+
{stakedPercent}% ${tokenInfo?.symbol} staked
-
-
-
+
+
+
{proposalsTotal} proposals created
-
-
+
+
@@ -248,7 +255,9 @@ function DaoHome() {
) : null}
setClaimsAvaliable((a) => a + n)}
+ incrementClaimsAvaliable={(_) =>
+ setWalletTokenBalanceUpdateCount((n) => n + 1)
+ }
stakingAddress={daoInfo.staking_contract}
tokenInfo={tokenInfo}
/>
@@ -256,7 +265,7 @@ function DaoHome() {
setTokenBalancesLoading(false)}
beforeExecute={() => setTokenBalancesLoading(true)}
- claimAmount={claimsAvaliable}
+ claimableTokens={claimsAvaliable}
contractAddress={contractAddress}
defaultMode={StakingMode.Stake}
onClose={() => setShowStaking(false)}
diff --git a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx
index 9105722a6..9f2c22b1b 100644
--- a/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx
+++ b/apps/dapp/pages/dao/[contractAddress]/proposals/[proposalId].tsx
@@ -3,12 +3,13 @@ import { useRouter } from 'next/router'
import { useRecoilValue } from 'recoil'
-import { Breadcrumbs } from 'components/Breadcrumbs'
+import { Breadcrumbs } from '@dao-dao/ui'
+
import { ProposalDetails } from 'components/ProposalDetails'
import {
ProposalDetailsSidebar,
- ProposalDetailsCard,
ProposalDetailsVoteStatus,
+ ProposalDetailsCard,
} from 'components/ProposalDetailsSidebar'
import { daoSelector } from 'selectors/daos'
import { cw20TokenInfo } from 'selectors/treasury'
diff --git a/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx b/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx
index 93b7c6642..8ac8602da 100644
--- a/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx
+++ b/apps/dapp/pages/dao/[contractAddress]/proposals/create.tsx
@@ -7,10 +7,9 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { findAttribute } from '@cosmjs/stargate/build/logs'
+import { CopyToClipboard, Breadcrumbs } from '@dao-dao/ui'
import toast from 'react-hot-toast'
-import { Breadcrumbs } from '@components/Breadcrumbs'
-import { CopyToClipboard } from '@components/CopyToClipboard'
import { ProposalData, ProposalForm } from '@components/ProposalForm'
import { proposalsCreatedAtom } from 'atoms/proposals'
import {
diff --git a/apps/dapp/pages/dao/create.tsx b/apps/dapp/pages/dao/create.tsx
index 0d8a0446b..b1ca0f948 100644
--- a/apps/dapp/pages/dao/create.tsx
+++ b/apps/dapp/pages/dao/create.tsx
@@ -6,31 +6,38 @@ import { useRouter } from 'next/router'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { InstantiateResult } from '@cosmjs/cosmwasm-stargate'
+import { Airplane } from '@dao-dao/icons'
import { TokenInfoResponse } from '@dao-dao/types/contracts/cw20-gov'
import { InstantiateMsg } from '@dao-dao/types/contracts/cw3-dao'
+import { Button, Tooltip } from '@dao-dao/ui'
+import {
+ AddressInput,
+ Breadcrumbs,
+ ImageSelector,
+ ImageSelectorModal,
+ InputErrorMessage,
+ InputLabel,
+ NumberInput,
+ TextareaInput,
+ TextInput,
+ ToggleInput,
+ TokenAmountInput,
+ GradientHero,
+} from '@dao-dao/ui'
+import {
+ DAO_CODE_ID,
+ NATIVE_DECIMALS,
+ convertDenomToMicroDenomWithDecimals,
+ secondsToWdhms,
+} from '@dao-dao/utils'
import { PlusIcon } from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
import { useFieldArray, useForm } from 'react-hook-form'
-import { Button } from 'ui'
-import { GradientHero } from '@components/ContractView'
import { FormCard } from '@components/FormCard'
-import SvgAirplane from '@components/icons/Airplane'
-import { AddressInput } from '@components/input/AddressInput'
-import { ImageSelector } from '@components/input/ImageSelector'
-import { ImageSelectorModal } from '@components/input/ImageSelector'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { InputLabel } from '@components/input/InputLabel'
-import { NumberInput } from '@components/input/NumberInput'
-import { TextareaInput } from '@components/input/TextAreaInput'
-import { TextInput } from '@components/input/TextInput'
-import { ToggleInput } from '@components/input/ToggleInput'
-import { TokenAmountInput } from '@components/input/TokenAmountInput'
import TooltipsDisplay, {
useTooltipsRegister,
} from '@components/TooltipsDisplay'
import { pinnedDaosAtom } from 'atoms/pinned'
-import { Breadcrumbs } from 'components/Breadcrumbs'
import {
daoCreateTooltipsGetter,
daoCreateTooltipsDefault,
@@ -40,11 +47,6 @@ import {
walletAddress as walletAddressSelector,
} from 'selectors/cosm'
import { cleanChainError } from 'util/cleanChainError'
-import { DAO_CODE_ID, NATIVE_DECIMALS } from 'util/constants'
-import {
- convertDenomToMicroDenomWithDecimals,
- secondsToWdhms,
-} from 'util/conversion'
import {
validateContractAddress,
validateNonNegative,
@@ -651,7 +653,7 @@ const CreateDao: NextPage = () => {
>
Submit{' '}
-
+
diff --git a/apps/dapp/pages/dao/list.tsx b/apps/dapp/pages/dao/list.tsx
index 1b55a5ddb..95cdd455d 100644
--- a/apps/dapp/pages/dao/list.tsx
+++ b/apps/dapp/pages/dao/list.tsx
@@ -12,6 +12,12 @@ import {
Loadable,
} from 'recoil'
+import { Button } from '@dao-dao/ui'
+import {
+ DAO_CODE_ID,
+ LEGACY_DAO_CODE_ID,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
import {
LibraryIcon,
PlusIcon,
@@ -19,8 +25,6 @@ import {
UserIcon,
} from '@heroicons/react/outline'
-import { Button } from '@components'
-
import { EmptyDaoCard } from '@components/EmptyDaoCard'
import { pinnedDaosAtom } from 'atoms/pinned'
import CodeIdSelect from 'components/CodeIdSelect'
@@ -29,8 +33,6 @@ import Paginator from 'components/Paginator'
import { pagedContractsByCodeId } from 'selectors/contracts'
import { DaoListType, memberDaoSelector } from 'selectors/daos'
import { addToken } from 'util/addToken'
-import { DAO_CODE_ID, LEGACY_DAO_CODE_ID } from 'util/constants'
-import { convertMicroDenomToDenomWithDecimals } from 'util/conversion'
export function DaoCard({
dao,
diff --git a/apps/dapp/pages/index.tsx b/apps/dapp/pages/index.tsx
index 595ac6f37..3ae0ebf1f 100644
--- a/apps/dapp/pages/index.tsx
+++ b/apps/dapp/pages/index.tsx
@@ -3,6 +3,9 @@ import { ReactNode } from 'react'
import type { NextPage } from 'next'
import Link from 'next/link'
+import { ArrowUpRight, Discord, Github, Twitter } from '@dao-dao/icons'
+import { Button, GradientWrapper, Logo } from '@dao-dao/ui'
+import { SITE_TITLE } from '@dao-dao/utils'
import { ScaleIcon } from '@heroicons/react/outline'
import {
ArrowNarrowRightIcon,
@@ -10,25 +13,15 @@ import {
StarIcon,
} from '@heroicons/react/solid'
-import { Button } from '@components'
-
-import SvgArrowUpRight from '@components/icons/ArrowUpRight'
-import SvgDiscord from '@components/icons/Discord'
-import { GradientWrapper } from 'components/GradientWrapper'
-import SvgGithub from 'components/icons/Github'
-import SvgTwitter from 'components/icons/Twitter'
-import { Logo } from 'components/Logo'
import ThemeToggle from 'components/ThemeToggle'
-import { SITE_TITLE } from '../util/constants'
-
function EnterAppButton({ small }: { small?: boolean }) {
return (
Enter the app{' '}
-
+
@@ -85,11 +78,7 @@ const Home: NextPage = () => {
href="https://docs.daodao.zone"
>
Documentation
-
+
diff --git a/apps/dapp/pages/multisig/[contractAddress]/index.tsx b/apps/dapp/pages/multisig/[contractAddress]/index.tsx
index d33e59740..9e237aee3 100644
--- a/apps/dapp/pages/multisig/[contractAddress]/index.tsx
+++ b/apps/dapp/pages/multisig/[contractAddress]/index.tsx
@@ -6,27 +6,28 @@ import { useRouter } from 'next/router'
import { useRecoilState, useRecoilValue } from 'recoil'
import { Threshold } from '@dao-dao/types/contracts/cw3-multisig'
+import {
+ CopyToClipboard,
+ useThemeContext,
+ GradientHero,
+ HorizontalInfo,
+ HorizontalInfoSection,
+ ContractHeader,
+ StarButton,
+ BalanceIcon,
+ Breadcrumbs,
+} from '@dao-dao/ui'
import {
ScaleIcon,
UserGroupIcon,
VariableIcon,
} from '@heroicons/react/outline'
-import { useThemeContext } from 'ui'
-import { CopyToClipboard } from '@components/CopyToClipboard'
import { MultisigContractInfo } from '@components/MultisigContractInfo'
import { pinnedMultisigsAtom } from 'atoms/pinned'
-import { Breadcrumbs } from 'components/Breadcrumbs'
-import {
- ContractProposalsDispaly,
- GradientHero,
- HeroContractHorizontalInfo,
- HeroContractHeader,
- StarButton,
- BalanceIcon,
- HeroContractHorizontalInfoSection,
-} from 'components/ContractView'
+import { ContractProposalsDispaly } from 'components/ContractView'
import ErrorBoundary from 'components/ErrorBoundary'
+import { contractInstantiateTime } from 'selectors/contracts'
import { CHAIN_RPC_ENDPOINT } from 'selectors/cosm'
import {
listMembers,
@@ -90,6 +91,7 @@ function MultisigHome() {
const contractAddress = router.query.contractAddress as string
const sigInfo = useRecoilValue(sigSelector(contractAddress))
+ const established = useRecoilValue(contractInstantiateTime(contractAddress))
const weightTotal = useRecoilValue(totalWeight(contractAddress))
const visitorWeight = useRecoilValue(memberWeight(contractAddress))
@@ -121,28 +123,28 @@ function MultisigHome() {
/>
-
-
-
+
+
{thresholdString(sigInfo.config.threshold)}
-
-
+
+
Total votes: {weightTotal}
-
-
+
+
Total members: {memberList.length}
-
-
+
+
@@ -207,7 +209,7 @@ const MultisigHomePage: NextPage = ({ accentColor }) => {
}, [accentColor, setAccentColor, isReady, isFallback])
// Trigger Suspense.
- if (!isReady || isFallback) throw new Promise((resolve) => {})
+ if (!isReady || isFallback) throw new Promise((_) => {})
return (
diff --git a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx
index ace15b90e..583c82170 100644
--- a/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx
+++ b/apps/dapp/pages/multisig/[contractAddress]/proposals/[proposalId].tsx
@@ -3,7 +3,8 @@ import { useRouter } from 'next/router'
import { useRecoilValue } from 'recoil'
-import { Breadcrumbs } from 'components/Breadcrumbs'
+import { Breadcrumbs } from '@dao-dao/ui'
+
import { ProposalDetails } from 'components/ProposalDetails'
import {
ProposalDetailsSidebar,
diff --git a/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx b/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx
index d88deb06d..9202c4bc2 100644
--- a/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx
+++ b/apps/dapp/pages/multisig/[contractAddress]/proposals/create.tsx
@@ -7,9 +7,9 @@ import { useRecoilValue } from 'recoil'
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { findAttribute } from '@cosmjs/stargate/build/logs'
+import { Breadcrumbs } from '@dao-dao/ui'
import toast from 'react-hot-toast'
-import { Breadcrumbs } from '@components/Breadcrumbs'
import { ProposalData, ProposalForm } from '@components/ProposalForm'
import {
cosmWasmSigningClient,
diff --git a/apps/dapp/pages/multisig/create.tsx b/apps/dapp/pages/multisig/create.tsx
index 2e210e3e8..3b5ea6863 100644
--- a/apps/dapp/pages/multisig/create.tsx
+++ b/apps/dapp/pages/multisig/create.tsx
@@ -6,26 +6,28 @@ import { useRouter } from 'next/router'
import { useSetRecoilState, useRecoilValue } from 'recoil'
import { InstantiateResult } from '@cosmjs/cosmwasm-stargate'
+import { Airplane } from '@dao-dao/icons'
+import { Button, Tooltip } from '@dao-dao/ui'
+import {
+ GradientHero,
+ Breadcrumbs,
+ ImageSelector,
+ InputErrorMessage,
+ InputLabel,
+ NumberInput,
+ TextareaInput,
+ TextInput,
+ TokenAmountInput,
+} from '@dao-dao/ui'
+import { MULTISIG_CODE_ID, secondsToWdhms } from '@dao-dao/utils'
import { PlusIcon } from '@heroicons/react/outline'
-import Tooltip from '@reach/tooltip'
import { useFieldArray, useForm, Validate } from 'react-hook-form'
-import { Button } from 'ui'
-import { GradientHero } from '@components/ContractView'
import { FormCard } from '@components/FormCard'
-import SvgAirplane from '@components/icons/Airplane'
-import { ImageSelector } from '@components/input/ImageSelector'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { InputLabel } from '@components/input/InputLabel'
-import { NumberInput } from '@components/input/NumberInput'
-import { TextareaInput } from '@components/input/TextAreaInput'
-import { TextInput } from '@components/input/TextInput'
-import { TokenAmountInput } from '@components/input/TokenAmountInput'
import TooltipsDisplay, {
useTooltipsRegister,
} from '@components/TooltipsDisplay'
import { pinnedMultisigsAtom } from 'atoms/pinned'
-import { Breadcrumbs } from 'components/Breadcrumbs'
import {
multisigCreateTooltipsDefault,
multisigCreateTooltipsGetter,
@@ -35,8 +37,6 @@ import {
walletAddress as walletAddressSelector,
} from 'selectors/cosm'
import { cleanChainError } from 'util/cleanChainError'
-import { MULTISIG_CODE_ID } from 'util/constants'
-import { secondsToWdhms } from 'util/conversion'
import {
validatePercent,
validatePositive,
@@ -296,7 +296,7 @@ const CreateMultisig: NextPage = () => {
>
Submit{' '}
-
+
diff --git a/apps/dapp/pages/multisig/list.tsx b/apps/dapp/pages/multisig/list.tsx
index aff3a6a7f..e4c4f74ef 100644
--- a/apps/dapp/pages/multisig/list.tsx
+++ b/apps/dapp/pages/multisig/list.tsx
@@ -12,13 +12,18 @@ import {
Loadable,
} from 'recoil'
+import { Button } from '@dao-dao/ui'
+import {
+ LEGACY_MULTISIG_CODE_ID,
+ MULTISIG_CODE_ID,
+ NATIVE_DENOM,
+} from '@dao-dao/utils'
import {
LibraryIcon,
PlusIcon,
SparklesIcon,
UserIcon,
} from '@heroicons/react/outline'
-import { Button } from 'ui'
import { EmptyMultisigCard } from '@components/EmptyMultisigCard'
import { pinnedMultisigsAtom } from 'atoms/pinned'
@@ -29,11 +34,6 @@ import { pagedContractsByCodeId } from 'selectors/contracts'
import { proposalCount } from 'selectors/daos'
import { MultisigListType, sigMemberSelector } from 'selectors/multisigs'
import { nativeBalance } from 'selectors/treasury'
-import {
- LEGACY_MULTISIG_CODE_ID,
- MULTISIG_CODE_ID,
- NATIVE_DENOM,
-} from 'util/constants'
export function MultisigCard({
multisig,
diff --git a/apps/dapp/pages/starred.tsx b/apps/dapp/pages/starred.tsx
index 2b428bbb1..7e272c60d 100644
--- a/apps/dapp/pages/starred.tsx
+++ b/apps/dapp/pages/starred.tsx
@@ -3,6 +3,10 @@ import Link from 'next/link'
import { useRecoilState, useRecoilValue } from 'recoil'
+import {
+ NATIVE_DENOM,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
import { MapIcon, PlusIcon, StarIcon } from '@heroicons/react/outline'
import { EmptyDaoCard } from '@components/EmptyDaoCard'
@@ -14,8 +18,6 @@ import { memberDaoSelector, proposalCount } from 'selectors/daos'
import { sigSelector } from 'selectors/multisigs'
import { cw20TokenInfo, nativeBalance } from 'selectors/treasury'
import { addToken } from 'util/addToken'
-import { NATIVE_DENOM } from 'util/constants'
-import { convertMicroDenomToDenomWithDecimals } from 'util/conversion'
function PinnedDaoCard({ address }: { address: string }) {
const listInfo = useRecoilValue(memberDaoSelector(address))
diff --git a/apps/dapp/selectors/cosm.ts b/apps/dapp/selectors/cosm.ts
index 97b7a89ff..fe70827a8 100644
--- a/apps/dapp/selectors/cosm.ts
+++ b/apps/dapp/selectors/cosm.ts
@@ -2,12 +2,12 @@ import { selector, selectorFamily, atom } from 'recoil'
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { GasPrice } from '@cosmjs/stargate'
+import { NATIVE_DENOM, GAS_PRICE } from '@dao-dao/utils'
import {
cosmWasmClientRouter,
stargateClientRouter,
} from 'util/chainClientRouter'
-import { NATIVE_DENOM, GAS_PRICE } from 'util/constants'
import { localStorageEffect } from '../atoms/localStorageEffect'
import { connectKeplrWithoutAlerts } from '../services/keplr'
@@ -125,6 +125,9 @@ export const walletChainBalanceSelector = selector
({
return 0
}
const address = get(walletAddress)
+ if (!address) {
+ return 0
+ }
const balance = await client.getBalance(address, NATIVE_DENOM)
return Number(balance.amount)
},
diff --git a/apps/dapp/selectors/daos.ts b/apps/dapp/selectors/daos.ts
index 50ecd6aac..73e7d8312 100644
--- a/apps/dapp/selectors/daos.ts
+++ b/apps/dapp/selectors/daos.ts
@@ -6,10 +6,10 @@ import {
ConfigResponse,
Duration,
} from '@dao-dao/types/contracts/cw3-dao'
+import { DAO_CODE_ID, NATIVE_DENOM } from '@dao-dao/utils'
import { contractsByCodeId } from 'selectors/contracts'
import { cosmWasmClient, isMemberSelector } from 'selectors/cosm'
-import { DAO_CODE_ID, NATIVE_DENOM } from 'util/constants'
import {
nativeBalance,
diff --git a/apps/dapp/selectors/multisigs.ts b/apps/dapp/selectors/multisigs.ts
index b9714873b..aeff7ce61 100644
--- a/apps/dapp/selectors/multisigs.ts
+++ b/apps/dapp/selectors/multisigs.ts
@@ -1,10 +1,10 @@
import { selectorFamily } from 'recoil'
import { ConfigResponse } from '@dao-dao/types/contracts/cw3-multisig'
+import { MULTISIG_CODE_ID } from '@dao-dao/utils'
import { contractsByCodeId } from 'selectors/contracts'
import { cosmWasmClient, isMemberSelector } from 'selectors/cosm'
-import { MULTISIG_CODE_ID } from 'util/constants'
import { walletAddress } from './treasury'
diff --git a/apps/dapp/selectors/proposals.ts b/apps/dapp/selectors/proposals.ts
index 5429b7252..f2a2906bb 100644
--- a/apps/dapp/selectors/proposals.ts
+++ b/apps/dapp/selectors/proposals.ts
@@ -8,25 +8,11 @@ import {
} from '@dao-dao/types/contracts/cw3-dao'
import {
- contractProposalMapAtom,
proposalsRequestIdAtom,
proposalUpdateCountAtom,
} from 'atoms/proposals'
-import { MessageMap, MessageMapEntry } from 'models/proposal/messageMap'
-import {
- EmptyProposalResponse,
- EmptyProposalTallyResponse,
- EmptyThresholdResponse,
-} from 'models/proposal/proposal'
-import {
- ContractProposalMap,
- ExtendedProposalResponse,
- ProposalKey,
- ProposalMap,
- ProposalMapItem,
- ProposalMessageKey,
-} from 'types/proposals'
+import { EmptyProposalTallyResponse } from '../models/proposal/proposal'
import { cosmWasmClient } from './cosm'
import { daoSelector } from './daos'
import { sigSelector } from './multisigs'
@@ -60,7 +46,7 @@ export enum WalletVote {
}
type WalletVoteValue = `${WalletVote}`
-export const onChainProposalsSelector = selectorFamily<
+export const proposalsSelector = selectorFamily<
ProposalResponse[],
{
contractAddress: string
@@ -90,7 +76,7 @@ export const onChainProposalsSelector = selectorFamily<
export const proposalSelector = selectorFamily<
ProposalResponse | undefined,
- { contractAddress: string; proposalId: string | number }
+ { contractAddress: string; proposalId: number }
>({
key: 'proposalSelector',
get:
@@ -99,20 +85,10 @@ export const proposalSelector = selectorFamily<
proposalId,
}: {
contractAddress: string
- proposalId: string | number
+ proposalId: number
}) =>
async ({ get }) => {
- if (typeof proposalId === 'string') {
- const draftProposal = get(
- draftProposalSelector({ contractAddress, proposalId })
- )
- if (draftProposal?.proposal) {
- return draftProposal?.proposal
- }
- }
- if (typeof proposalId === 'number') {
- get(proposalUpdateCountAtom({ contractAddress, proposalId }))
- }
+ get(proposalUpdateCountAtom({ contractAddress, proposalId }))
const client = get(cosmWasmClient)
try {
@@ -272,9 +248,7 @@ export const proposalTallySelector = selectorFamily<
return tally
} catch (e) {
console.error(e)
- return {
- ...EmptyProposalTallyResponse,
- }
+ return { ...EmptyProposalTallyResponse }
}
},
})
@@ -319,144 +293,3 @@ export const votingPowerAtHeightSelector = selectorFamily<
return balance
},
})
-
-export const draftProposalsSelector = selectorFamily({
- key: 'draftProposals',
- get:
- (contractAddress) =>
- ({ get }) => {
- return get(proposalMapSelector(contractAddress))
- },
- set:
- (contractAddress) =>
- ({ set }, newValue) => {
- set(proposalMapSelector(contractAddress), newValue)
- },
-})
-
-export const proposalsSelector = selectorFamily<
- ExtendedProposalResponse[],
- {
- contractAddress: string
- startBefore: number
- limit: number
- }
->({
- key: 'proposals',
- get:
- ({ contractAddress, startBefore, limit }) =>
- async ({ get }) => {
- let draftProposalItems: ExtendedProposalResponse[] = []
- // Add in draft proposals:
- const draftProposals = get(draftProposalsSelector(contractAddress))
- if (draftProposals) {
- draftProposalItems = Object.values(draftProposals).map((draft) => {
- const proposalResponse: ExtendedProposalResponse = {
- ...EmptyProposalResponse,
- ...draft.proposal,
- status: 'draft' as any,
- draftId: draft.id,
- threshold: { ...EmptyThresholdResponse },
- total_weight: 0,
- }
- return proposalResponse
- })
- }
-
- const onChainProposalList = get(
- onChainProposalsSelector({
- contractAddress,
- startBefore,
- limit,
- })
- )
-
- return (draftProposalItems ?? []).concat(onChainProposalList)
- },
-})
-
-export const proposalMapSelector = selectorFamily({
- key: 'proposalMap',
- get:
- (contractAddress) =>
- ({ get }) => {
- const contractProposalMap = get(contractProposalMapAtom)
- return contractProposalMap[contractAddress]
- },
- set:
- (contractAddress) =>
- ({ get, set }, newValue) => {
- const contractProposalMap = get(contractProposalMapAtom)
- const updatedMap: ContractProposalMap = {
- ...contractProposalMap,
- [contractAddress]: newValue,
- } as unknown as ContractProposalMap
- set(contractProposalMapAtom, updatedMap)
- },
-})
-
-export const draftProposalSelector = selectorFamily<
- ProposalMapItem | undefined,
- ProposalKey
->({
- key: 'draftProposal',
- get:
- ({ contractAddress, proposalId }) =>
- ({ get }) => {
- const draftProposals = get(proposalMapSelector(contractAddress))
- return draftProposals ? draftProposals[proposalId] : undefined
- },
- set:
- ({ contractAddress, proposalId }) =>
- ({ set, get }, newValue) => {
- if (newValue) {
- const draftProposals = get(proposalMapSelector(contractAddress))
- const updatedDraftProposals: ProposalMap = {
- ...draftProposals,
- [proposalId]: newValue as any,
- }
- set(proposalMapSelector(contractAddress), updatedDraftProposals)
- }
- },
-})
-
-export const draftProposalMessageSelector = selectorFamily<
- MessageMapEntry | undefined,
- ProposalMessageKey
->({
- key: 'draftProposalMessage',
- get:
- ({ contractAddress, proposalId, messageId }) =>
- ({ get }) => {
- const draftProposal = get(
- draftProposalSelector({ contractAddress, proposalId })
- )
- return draftProposal?.messages
- ? draftProposal.messages[messageId]
- : undefined
- },
- set:
- ({ contractAddress, proposalId, messageId }) =>
- ({ set, get }, newValue) => {
- if (!newValue) {
- return
- }
- const draftProposal = get(
- draftProposalSelector({ contractAddress, proposalId })
- )
- if (draftProposal) {
- const messages: MessageMap = {
- ...draftProposal?.messages,
- [messageId]: newValue,
- } as any
- const updatedProposal: ProposalMapItem = {
- ...draftProposal,
- messages,
- }
- set(
- draftProposalSelector({ contractAddress, proposalId }),
- updatedProposal
- )
- }
- },
-})
diff --git a/apps/dapp/services/keplr.tsx b/apps/dapp/services/keplr.tsx
index 6e575a034..356a294bf 100644
--- a/apps/dapp/services/keplr.tsx
+++ b/apps/dapp/services/keplr.tsx
@@ -1,7 +1,6 @@
+import { convertFromMicroDenom } from '@dao-dao/utils'
import { Keplr, Window as KeplrWindow } from '@keplr-wallet/types'
-import { convertFromMicroDenom } from '../util/conversion'
-
declare global {
interface Window extends KeplrWindow {}
}
diff --git a/apps/dapp/tailwind.config.js b/apps/dapp/tailwind.config.js
index a9d7350f6..e338a2fba 100644
--- a/apps/dapp/tailwind.config.js
+++ b/apps/dapp/tailwind.config.js
@@ -1,4 +1,4 @@
-let config = require('ui/tailwind.config')
+let config = require('@dao-dao/ui/tailwind.config')
module.exports = {
...config,
diff --git a/apps/dapp/templates/addToken.tsx b/apps/dapp/templates/addToken.tsx
index 7f86d0fca..89d2a25b9 100644
--- a/apps/dapp/templates/addToken.tsx
+++ b/apps/dapp/templates/addToken.tsx
@@ -2,13 +2,15 @@ import { useEffect } from 'react'
import { useRecoilValueLoadable } from 'recoil'
+import {
+ AddressInput,
+ InputErrorMessage,
+ InputLabel,
+ LogoNoBorder,
+} from '@dao-dao/ui'
import { XIcon } from '@heroicons/react/outline'
import { useFormContext } from 'react-hook-form'
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { InputLabel } from '@components/input/InputLabel'
-import { LogoNoBorder } from '@components/Logo'
import { tokenConfig } from 'selectors/daos'
import { Config } from 'util/contractConfigWrapper'
import { validateContractAddress, validateRequired } from 'util/formValidation'
@@ -116,9 +118,9 @@ export const TokenSelector = ({
clearErrors(getLabel('to'))}
+ clearError={() => clearErrors(getLabel('address'))}
setError={(message) =>
- setError(getLabel('to'), { type: 'manual', message })
+ setError(getLabel('address'), { type: 'manual', message })
}
/>
diff --git a/apps/dapp/templates/changeMembers.tsx b/apps/dapp/templates/changeMembers.tsx
index 2bd66387e..279e6db04 100644
--- a/apps/dapp/templates/changeMembers.tsx
+++ b/apps/dapp/templates/changeMembers.tsx
@@ -1,13 +1,11 @@
import { useRecoilValue } from 'recoil'
+import { Button } from '@dao-dao/ui'
+import { AddressInput, InputErrorMessage, TokenAmountInput } from '@dao-dao/ui'
import { PlusIcon, UserIcon, XIcon } from '@heroicons/react/outline'
import { useFieldArray, useFormContext } from 'react-hook-form'
-import { Button } from 'ui/Button'
import { FormCard } from '@components/FormCard'
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { TokenAmountInput } from '@components/input/TokenAmountInput'
import { listMembers } from 'selectors/multisigs'
import { Config } from 'util/contractConfigWrapper'
import { validateAddress, validateRequired } from 'util/formValidation'
diff --git a/apps/dapp/templates/configUpdate.tsx b/apps/dapp/templates/configUpdate.tsx
index b4e12e38e..d9e34eec1 100644
--- a/apps/dapp/templates/configUpdate.tsx
+++ b/apps/dapp/templates/configUpdate.tsx
@@ -1,20 +1,22 @@
import { Config as DAOConfig } from '@dao-dao/types/contracts/cw3-dao'
-import { InformationCircleIcon, XIcon } from '@heroicons/react/outline'
-import { useFormContext } from 'react-hook-form'
-
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { InputLabel } from '@components/input/InputLabel'
-import { NumberInput } from '@components/input/NumberInput'
-import { TextInput } from '@components/input/TextInput'
-import { ToggleInput } from '@components/input/ToggleInput'
-import { DEFAULT_MAX_VOTING_PERIOD_SECONDS } from 'pages/dao/create'
-import { Config } from 'util/contractConfigWrapper'
+import {
+ InputErrorMessage,
+ InputLabel,
+ NumberInput,
+ TextInput,
+ ToggleInput,
+} from '@dao-dao/ui'
import {
secondsToWdhms,
convertDenomToMicroDenomWithDecimals,
convertMicroDenomToDenomWithDecimals,
getDaoThresholdAndQuorum,
-} from 'util/conversion'
+} from '@dao-dao/utils'
+import { InformationCircleIcon, XIcon } from '@heroicons/react/outline'
+import { useFormContext } from 'react-hook-form'
+
+import { DEFAULT_MAX_VOTING_PERIOD_SECONDS } from 'pages/dao/create'
+import { Config } from 'util/contractConfigWrapper'
import {
validatePercent,
validatePositive,
@@ -29,11 +31,6 @@ import {
ToCosmosMsgProps,
} from './templateList'
-enum ThresholdMode {
- Threshold,
- ThresholdQuorum,
-}
-
export interface DAOConfigUpdateData {
name: string
description: string
diff --git a/apps/dapp/templates/custom.tsx b/apps/dapp/templates/custom.tsx
index ff8a86f6d..07969a58f 100644
--- a/apps/dapp/templates/custom.tsx
+++ b/apps/dapp/templates/custom.tsx
@@ -1,8 +1,8 @@
+import { CodeMirrorInput } from '@dao-dao/ui'
import { CheckIcon, XIcon } from '@heroicons/react/outline'
import JSON5 from 'json5'
import { useFormContext } from 'react-hook-form'
-import { CodeMirrorInput } from '@components/input/CodeMirrorInput'
import { Config } from 'util/contractConfigWrapper'
import { makeWasmMessage } from 'util/messagehelpers'
import { validateCosmosMsg } from 'util/validateWasmMsg'
diff --git a/apps/dapp/templates/mint.tsx b/apps/dapp/templates/mint.tsx
index 4af58d6e8..758d7ed24 100644
--- a/apps/dapp/templates/mint.tsx
+++ b/apps/dapp/templates/mint.tsx
@@ -1,20 +1,18 @@
import { useRecoilValue } from 'recoil'
+import { AddressInput, InputErrorMessage, NumberInput } from '@dao-dao/ui'
+import {
+ convertDenomToMicroDenomWithDecimals,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
import { XIcon } from '@heroicons/react/outline'
import { useFormContext } from 'react-hook-form'
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { NumberInput } from '@components/input/NumberInput'
import {
Config,
contractConfigSelector,
ContractConfigWrapper,
} from 'util/contractConfigWrapper'
-import {
- convertDenomToMicroDenomWithDecimals,
- convertMicroDenomToDenomWithDecimals,
-} from 'util/conversion'
import {
validateAddress,
validatePositive,
diff --git a/apps/dapp/templates/removeToken.tsx b/apps/dapp/templates/removeToken.tsx
index d11792f00..f0d8f72f4 100644
--- a/apps/dapp/templates/removeToken.tsx
+++ b/apps/dapp/templates/removeToken.tsx
@@ -1,12 +1,10 @@
import { useRecoilValue, waitForAll } from 'recoil'
+import { Button } from '@dao-dao/ui'
+import { AddressInput, InputErrorMessage, InputLabel } from '@dao-dao/ui'
import { XIcon } from '@heroicons/react/outline'
import { useFormContext } from 'react-hook-form'
-import { Button } from 'ui/Button'
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { InputLabel } from '@components/input/InputLabel'
import { cw20TokenInfo, cw20TokensList } from 'selectors/treasury'
import { Config } from 'util/contractConfigWrapper'
import { validateContractAddress, validateRequired } from 'util/formValidation'
diff --git a/apps/dapp/templates/spend.tsx b/apps/dapp/templates/spend.tsx
index 71725bf5d..028c71f7b 100644
--- a/apps/dapp/templates/spend.tsx
+++ b/apps/dapp/templates/spend.tsx
@@ -1,27 +1,30 @@
import { useRecoilValue, waitForAll } from 'recoil'
+import {
+ AddressInput,
+ InputErrorMessage,
+ NumberInput,
+ SelectInput,
+} from '@dao-dao/ui'
+import {
+ NATIVE_DECIMALS,
+ NATIVE_DENOM,
+ convertDenomToHumanReadableDenom,
+ convertDenomToMicroDenomWithDecimals,
+ convertMicroDenomToDenomWithDecimals,
+ nativeTokenDecimals,
+ nativeTokenLabel,
+} from '@dao-dao/utils'
import { XIcon } from '@heroicons/react/outline'
import { useFormContext } from 'react-hook-form'
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { NumberInput } from '@components/input/NumberInput'
-import { SelectInput } from '@components/input/SelectInput'
import {
cw20TokensList,
cw20TokenInfo,
nativeBalance as nativeBalanceSelector,
cw20Balances as cw20BalancesSelector,
} from 'selectors/treasury'
-import { NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants'
import { Config } from 'util/contractConfigWrapper'
-import {
- convertDenomToHumanReadableDenom,
- convertDenomToMicroDenomWithDecimals,
- convertMicroDenomToDenomWithDecimals,
- nativeTokenDecimals,
- nativeTokenLabel,
-} from 'util/conversion'
import {
validateAddress,
validatePositive,
diff --git a/apps/dapp/templates/stake.tsx b/apps/dapp/templates/stake.tsx
index b04f8f2e5..59754aad5 100644
--- a/apps/dapp/templates/stake.tsx
+++ b/apps/dapp/templates/stake.tsx
@@ -1,22 +1,25 @@
import { useRecoilValue } from 'recoil'
-import { InformationCircleIcon, XIcon } from '@heroicons/react/outline'
-import { useFormContext } from 'react-hook-form'
-
-import { AddressInput } from '@components/input/AddressInput'
-import { InputErrorMessage } from '@components/input/InputErrorMessage'
-import { NumberInput } from '@components/input/NumberInput'
-import { SelectInput } from '@components/input/SelectInput'
-import { nativeBalance as nativeBalanceSelector } from 'selectors/treasury'
-import { NATIVE_DECIMALS, NATIVE_DENOM } from 'util/constants'
-import { Config } from 'util/contractConfigWrapper'
import {
+ AddressInput,
+ InputErrorMessage,
+ NumberInput,
+ SelectInput,
+} from '@dao-dao/ui'
+import {
+ NATIVE_DECIMALS,
+ NATIVE_DENOM,
convertDenomToHumanReadableDenom,
convertDenomToMicroDenomWithDecimals,
convertMicroDenomToDenomWithDecimals,
nativeTokenDecimals,
nativeTokenLabel,
-} from 'util/conversion'
+} from '@dao-dao/utils'
+import { InformationCircleIcon, XIcon } from '@heroicons/react/outline'
+import { useFormContext } from 'react-hook-form'
+
+import { nativeBalance as nativeBalanceSelector } from 'selectors/treasury'
+import { Config } from 'util/contractConfigWrapper'
import {
validateValidatorAddress,
validatePositive,
diff --git a/apps/dapp/templates/templateList.tsx b/apps/dapp/templates/templateList.tsx
index 7b02a6cbe..bd0473ea6 100644
--- a/apps/dapp/templates/templateList.tsx
+++ b/apps/dapp/templates/templateList.tsx
@@ -158,7 +158,8 @@ export const messageTemplateAndValuesForDecodedCosmosMsg = (
msg: Record,
props: FromCosmosMsgProps
) => {
- // Ensure custom is the last message template since it will match most proposals and we return the first successful message match.
+ // Ensure custom is the last message template since it will match most
+ // proposals and we return the first successful message match.
for (const template of messageTemplates) {
const values = template.fromCosmosMsg(msg, props)
if (values) {
@@ -180,6 +181,7 @@ export interface TemplateComponentProps {
multisig?: boolean
readOnly?: boolean
}
+
export type TemplateComponent = React.FunctionComponent
// Defines a new template.
diff --git a/apps/dapp/types/proposals.ts b/apps/dapp/types/proposals.ts
deleted file mode 100644
index 0a08882e8..000000000
--- a/apps/dapp/types/proposals.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Proposal, ProposalResponse } from '@dao-dao/types/contracts/cw3-dao'
-
-import { MessageMap } from 'models/proposal/messageMap'
-
-export type ProposalKey = {
- contractAddress: string
- proposalId: string
-}
-
-export type ProposalMessageKey = {
- contractAddress: string
- proposalId: string
- messageId: string
-}
-
-export interface ProposalMapItem {
- proposal: Proposal
- id: string
- activeMessageIndex?: number
- draft: boolean
- messages?: MessageMap
-}
-
-// Maps from a proposal id (a stringified number)
-// to a ProposalMapItem
-export type ProposalMap = {
- [key: string]: ProposalMapItem
-}
-
-// Maps from a contract address to a map of its draft
-// proposals
-export type ContractProposalMap = {
- [key: string]: ProposalMap
-}
-
-export interface ExtendedProposalResponse extends ProposalResponse {
- draftId?: string
-}
diff --git a/apps/dapp/util/formValidation.ts b/apps/dapp/util/formValidation.ts
index bc062f312..c6fcffc3c 100644
--- a/apps/dapp/util/formValidation.ts
+++ b/apps/dapp/util/formValidation.ts
@@ -42,7 +42,7 @@ export const validateContractAddress = (v: string) =>
export const validateJSON = (v: string) => {
try {
- const o = JSON5.parse(v)
+ JSON5.parse(v)
return true
} catch (e: any) {
return e?.message as string
diff --git a/apps/dapp/util/messagehelpers.ts b/apps/dapp/util/messagehelpers.ts
index 10e7b01c9..3eb3e53fa 100644
--- a/apps/dapp/util/messagehelpers.ts
+++ b/apps/dapp/util/messagehelpers.ts
@@ -1,5 +1,3 @@
-import { useRecoilValue } from 'recoil'
-
import { toBase64, toAscii } from '@cosmjs/encoding'
import { ExecuteMsg as MintExecuteMsg } from '@dao-dao/types/contracts/cw20-gov'
import {
@@ -8,7 +6,6 @@ import {
StakingMsg,
DistributionMsg,
CosmosMsgFor_Empty,
- ExecuteMsg,
InstantiateMsg as DaoInstantiateMsg,
Cw20Coin,
Duration,
@@ -20,21 +17,19 @@ import {
InstantiateMsg as MultisigInstantiateMsg,
Member,
} from '@dao-dao/types/contracts/cw3-multisig'
+import {
+ C4_GROUP_CODE_ID,
+ CW20_CODE_ID,
+ STAKE_CODE_ID,
+ convertDenomToHumanReadableDenom,
+} from '@dao-dao/utils'
import { MintMsg } from 'types/messages'
-import { ProposalMapItem } from 'types/proposals'
import {
MessageMapEntry,
ProposalMessageType,
} from '../models/proposal/messageMap'
-import { cw20TokenInfo } from '../selectors/treasury'
-import { C4_GROUP_CODE_ID, CW20_CODE_ID, STAKE_CODE_ID } from './constants'
-import { convertDenomToContractReadableDenom } from './conversion'
-import {
- convertDenomToHumanReadableDenom,
- convertDenomToMicroDenomWithDecimals,
-} from './conversion'
const DENOM = convertDenomToHumanReadableDenom(
process.env.NEXT_PUBLIC_STAKING_DENOM || ''
@@ -355,45 +350,6 @@ export interface MessageAction {
isEnabled: () => boolean
}
-export function labelForAmount(amount: Coin[]): string {
- if (!amount?.length) {
- return ''
- }
- return amount
- .map((coin) => `${coin.amount !== '' ? coin.amount : '0'} ${coin.denom}`)
- .join(', ')
-}
-
-export function labelForMessage(
- msg?: CosmosMsgFor_Empty | ExecuteMsg | MintExecuteMsg,
- defaultMessage = ''
-): string {
- if (!msg) {
- return defaultMessage
- }
- // TODO(gavin.doughtie): i18n
- const anyMsg: any = msg
- let messageString = ''
- if (anyMsg.bank) {
- if (anyMsg.bank.send) {
- messageString = `${labelForAmount(anyMsg.bank.send.amount)} -> ${
- anyMsg.bank.send.to_address
- }`
- } else if (anyMsg.bank.burn) {
- messageString = `${labelForAmount(anyMsg.bank.burn.amount)} -> 🔥`
- }
- } else if (anyMsg.mint) {
- messageString = `${anyMsg.mint.amount} -> ${anyMsg.mint.recipient}`
- } else if (anyMsg.custom) {
- const customMap: { [k: string]: any } = anyMsg.custom
- messageString = Object.entries(customMap)
- .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
- .join(', ')
- messageString = messageString.slice(0, MAX_LABEL_LEN) || ''
- }
- return messageString
-}
-
export function parseEncodedMessage(base64String?: string) {
if (base64String) {
const jsonMessage = decodeURIComponent(escape(atob(base64String)))
@@ -531,80 +487,6 @@ export function isMintMsg(msg: any): msg is MintMsg {
return false
}
-export function useMessageForDraftProposal(
- draftProposal: ProposalMapItem,
- govTokenAddress?: string
-) {
- const govTokenInfo = useRecoilValue(cw20TokenInfo(govTokenAddress as string))
- const msgs = draftProposal.messages
- ? Object.values(draftProposal.messages).map((mapEntry) => {
- // Spend proposals are inputted in human readable form (ex:
- // junox). Contracts expect things in the micro form (ex: ujunox)
- // so we, painfully, do some conversions:
- if (mapEntry.messageType === ProposalMessageType.Spend) {
- // Without doing a deep copy here we run the risk of modifying
- // fields of the message which are displayed in the UI.
- let microMessage = JSON.parse(JSON.stringify(mapEntry.message))
- const bank = (microMessage as any).bank as BankMsg
- if (!bank) {
- return
- }
-
- let amounts: Coin[]
- let variant: string
- if ('send' in bank) {
- amounts = (bank as any).send.amount
- variant = 'send'
- } else if ('burn' in bank) {
- amounts = (bank as any).burn.amount
- variant = 'burn'
- } else {
- console.error(`unexpected bank message: (${JSON.stringify(bank)})`)
- return
- }
-
- const microAmounts = amounts.map((coin) => {
- const microCoin = coin
- microCoin.amount = convertDenomToMicroDenomWithDecimals(
- coin.amount,
- govTokenInfo.decimals
- )
- microCoin.denom = convertDenomToContractReadableDenom(coin.denom)
- return microCoin
- }) as Coin[]
-
- ;(((microMessage as any).bank as any)[variant] as any).amount =
- microAmounts
-
- return microMessage
- }
- if (mapEntry.messageType === ProposalMessageType.Mint) {
- const mintMessage = JSON.parse(JSON.stringify(mapEntry.message))
- console.log(mintMessage)
- if (mintMessage?.mint?.amount) {
- mintMessage.mint.amount = convertDenomToMicroDenomWithDecimals(
- mintMessage.mint.amount,
- govTokenInfo.decimals
- )
- }
- return makeExecutableMintMessage(
- mintMessage,
- govTokenAddress as string
- )
- }
- return mapEntry.message
- })
- : []
- const proposal = draftProposal.proposal
-
- const msg: Record = {
- title: proposal.title,
- description: proposal.description,
- msgs,
- }
- return msg as any
-}
-
export const getMintRecipient = (mintMsg?: MessageMapEntry) => {
return mintMsg?.message?.mint?.recipient
}
diff --git a/apps/dapp/util/proposal.ts b/apps/dapp/util/proposal.ts
index 90c56d353..e47f7b6ed 100644
--- a/apps/dapp/util/proposal.ts
+++ b/apps/dapp/util/proposal.ts
@@ -1,156 +1,13 @@
-import { TransactionInterface_UNSTABLE, useRecoilValue } from 'recoil'
+import { useRecoilValue } from 'recoil'
-import { Proposal, ProposalResponse } from '@dao-dao/types/contracts/cw3-dao'
+import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils'
-import {
- contractProposalMapAtom,
- nextDraftProposalIdAtom,
-} from 'atoms/proposals'
-import {
- EmptyProposalResponse,
- EmptyThresholdResponse,
-} from 'models/proposal/proposal'
-import {
- draftProposalsSelector,
- proposalTallySelector,
-} from 'selectors/proposals'
-import {
- ContractProposalMap,
- ExtendedProposalResponse,
- ProposalMap,
- ProposalMapItem,
-} from 'types/proposals'
+import { proposalTallySelector } from 'selectors/proposals'
import {
contractConfigSelector,
ContractConfigWrapper,
} from './contractConfigWrapper'
-import { convertMicroDenomToDenomWithDecimals } from './conversion'
-
-// Prefix used in IDs for draft proposals
-const DRAFT_PROPOSAL_PREFFIX = 'draft:'
-
-export function isProposal(
- proposal: Proposal | ProposalResponse | ProposalMapItem | undefined
-): proposal is Proposal {
- if (!proposal) {
- return false
- }
- if ((proposal as ProposalMapItem)?.draft === false) {
- return false
- }
- return (proposal as Proposal).proposer !== undefined
-}
-
-export function draftProposalKey(proposalId: number): string {
- return `${DRAFT_PROPOSAL_PREFFIX}${proposalId}`
-}
-
-// Convenience function to create a draft proposal entry
-export function draftProposalItem(
- proposal: Proposal,
- id: string
-): ProposalMapItem {
- return {
- proposal,
- id,
- draft: true,
- }
-}
-
-export function isDraftProposalKey(proposalKey: string): boolean {
- return proposalKey.startsWith(DRAFT_PROPOSAL_PREFFIX)
-}
-
-export function draftProposalKeyNumber(proposalKey: string): number {
- if (isDraftProposalKey(proposalKey)) {
- const str = proposalKey.substring(
- proposalKey.indexOf(DRAFT_PROPOSAL_PREFFIX)
- )
- try {
- const num = parseInt(str)
- return num
- } catch (e) {
- console.error(e)
- }
- }
- return -1
-}
-
-export const createDraftProposalTransaction =
- (contractAddress: string, draftProposals: ProposalMap) =>
- ({ get, set }: TransactionInterface_UNSTABLE) => {
- const contractProposalMap = get(contractProposalMapAtom)
- const setContractProposalMap = (
- contractProposalMap: ContractProposalMap
- ) => {
- set(contractProposalMapAtom, contractProposalMap)
- }
- const nextDraftProposalId = get(nextDraftProposalIdAtom)
- const incrementDraftProposalId = () =>
- set(nextDraftProposalIdAtom, nextDraftProposalId + 1)
-
- return ({ draftProposal }: { draftProposal: Proposal }) => {
- const proposalKey = draftProposalKey(nextDraftProposalId)
- setContractProposalMap({
- ...contractProposalMap,
- [contractAddress]: {
- ...draftProposals,
- [proposalKey]: draftProposalItem(draftProposal, proposalKey),
- },
- })
- incrementDraftProposalId()
- }
- }
-
-export const deleteDraftProposal = (
- draftProposals: ProposalMap,
- proposalId: string
-) => {
- const updatedProposals = { ...draftProposals }
- delete updatedProposals[proposalId + '']
- return updatedProposals
-}
-
-export const deleteDraftProposalTransaction =
- ({
- contractAddress,
- proposalId,
- }: {
- contractAddress: string
- proposalId: string
- }) =>
- ({ get, set }: TransactionInterface_UNSTABLE) => {
- const draftProposals = get(draftProposalsSelector(contractAddress))
-
- return () => {
- const updatedProposals = { ...draftProposals }
- delete updatedProposals[proposalId + '']
- set(draftProposalsSelector(contractAddress), updatedProposals)
- }
- }
-
-export const draftProposalToExtendedResponse = (draft: ProposalMapItem) => {
- const proposalResponse: ExtendedProposalResponse = {
- ...EmptyProposalResponse,
- ...draft.proposal,
- status: 'draft' as any,
- draftId: draft.id,
- threshold: { ...EmptyThresholdResponse },
- total_weight: 0,
- }
- return proposalResponse
-}
-
-export const draftProposalsToExtendedResponses = (
- draftProposals: ProposalMap
-) => {
- return draftProposals
- ? Object.values(draftProposals).map((draft) =>
- draftProposalToExtendedResponse(draft)
- )
- : []
-}
export const useThresholdQuorum = (
contractAddress: string,
diff --git a/apps/dapp/assets/icons/Abstain.svg b/packages/icons/assets/Abstain.svg
similarity index 100%
rename from apps/dapp/assets/icons/Abstain.svg
rename to packages/icons/assets/Abstain.svg
diff --git a/apps/dapp/assets/icons/Discord.svg b/packages/icons/assets/Discord.svg
similarity index 100%
rename from apps/dapp/assets/icons/Discord.svg
rename to packages/icons/assets/Discord.svg
diff --git a/apps/dapp/assets/icons/MemberCheck.svg b/packages/icons/assets/MemberCheck.svg
similarity index 100%
rename from apps/dapp/assets/icons/MemberCheck.svg
rename to packages/icons/assets/MemberCheck.svg
diff --git a/apps/dapp/components/icons/TriangleUp.tsx b/packages/icons/assets/TriangleUp.svg
similarity index 58%
rename from apps/dapp/components/icons/TriangleUp.tsx
rename to packages/icons/assets/TriangleUp.svg
index 0cc29e53b..b79e42af7 100644
--- a/apps/dapp/components/icons/TriangleUp.tsx
+++ b/packages/icons/assets/TriangleUp.svg
@@ -1,17 +1,11 @@
-import * as React from 'react'
-import { SVGProps } from 'react'
-
-export const TriangleUp = (props: SVGProps) => (
-)
diff --git a/apps/dapp/assets/icons/airplane.svg b/packages/icons/assets/airplane.svg
similarity index 100%
rename from apps/dapp/assets/icons/airplane.svg
rename to packages/icons/assets/airplane.svg
diff --git a/apps/dapp/assets/icons/arrowUpRight.svg b/packages/icons/assets/arrowUpRight.svg
similarity index 100%
rename from apps/dapp/assets/icons/arrowUpRight.svg
rename to packages/icons/assets/arrowUpRight.svg
diff --git a/apps/dapp/assets/icons/copy.svg b/packages/icons/assets/copy.svg
similarity index 87%
rename from apps/dapp/assets/icons/copy.svg
rename to packages/icons/assets/copy.svg
index 59e7dc3c1..fe675c746 100644
--- a/apps/dapp/assets/icons/copy.svg
+++ b/packages/icons/assets/copy.svg
@@ -1,5 +1,5 @@
-
-
-
+
+
+
diff --git a/apps/dapp/assets/icons/dao.svg b/packages/icons/assets/dao.svg
similarity index 100%
rename from apps/dapp/assets/icons/dao.svg
rename to packages/icons/assets/dao.svg
diff --git a/apps/dapp/assets/icons/executed.svg b/packages/icons/assets/executed.svg
similarity index 100%
rename from apps/dapp/assets/icons/executed.svg
rename to packages/icons/assets/executed.svg
diff --git a/apps/dapp/assets/icons/github.svg b/packages/icons/assets/github.svg
similarity index 100%
rename from apps/dapp/assets/icons/github.svg
rename to packages/icons/assets/github.svg
diff --git a/apps/dapp/assets/icons/message.svg b/packages/icons/assets/message.svg
similarity index 100%
rename from apps/dapp/assets/icons/message.svg
rename to packages/icons/assets/message.svg
diff --git a/apps/dapp/assets/icons/open.svg b/packages/icons/assets/open.svg
similarity index 100%
rename from apps/dapp/assets/icons/open.svg
rename to packages/icons/assets/open.svg
diff --git a/apps/dapp/assets/icons/passed.svg b/packages/icons/assets/passed.svg
similarity index 100%
rename from apps/dapp/assets/icons/passed.svg
rename to packages/icons/assets/passed.svg
diff --git a/apps/dapp/assets/icons/pencil.svg b/packages/icons/assets/pencil.svg
similarity index 100%
rename from apps/dapp/assets/icons/pencil.svg
rename to packages/icons/assets/pencil.svg
diff --git a/apps/dapp/assets/icons/rejected.svg b/packages/icons/assets/rejected.svg
similarity index 100%
rename from apps/dapp/assets/icons/rejected.svg
rename to packages/icons/assets/rejected.svg
diff --git a/apps/dapp/assets/icons/twitter.svg b/packages/icons/assets/twitter.svg
similarity index 95%
rename from apps/dapp/assets/icons/twitter.svg
rename to packages/icons/assets/twitter.svg
index 1cb8a6447..5ec228814 100644
--- a/apps/dapp/assets/icons/twitter.svg
+++ b/packages/icons/assets/twitter.svg
@@ -1,3 +1,3 @@
-
+
diff --git a/apps/dapp/assets/icons/votes.svg b/packages/icons/assets/votes.svg
similarity index 100%
rename from apps/dapp/assets/icons/votes.svg
rename to packages/icons/assets/votes.svg
diff --git a/packages/ui/components/Breadcrumbs.tsx b/packages/ui/components/Breadcrumbs.tsx
new file mode 100644
index 000000000..1fc99ac00
--- /dev/null
+++ b/packages/ui/components/Breadcrumbs.tsx
@@ -0,0 +1,29 @@
+import { FC } from 'react'
+
+import Link from 'next/link'
+
+import { ArrowNarrowLeftIcon } from '@heroicons/react/outline'
+
+export interface BreadcrumbsProps {
+ crumbs: Array<[string, string]>
+}
+
+export const Breadcrumbs: FC = ({ crumbs }) => (
+
+
+
+
+
+
+
+
+ {crumbs.map(([link, name], idx) => (
+
+
+ {name}
+
+ {idx != crumbs.length - 1 && '/'}
+
+ ))}
+
+)
diff --git a/packages/ui/Button/Button.stories.tsx b/packages/ui/components/Button/Button.stories.tsx
similarity index 100%
rename from packages/ui/Button/Button.stories.tsx
rename to packages/ui/components/Button/Button.stories.tsx
diff --git a/packages/ui/Button/Button.tsx b/packages/ui/components/Button/Button.tsx
similarity index 98%
rename from packages/ui/Button/Button.tsx
rename to packages/ui/components/Button/Button.tsx
index 23e6a2287..b30243efd 100644
--- a/packages/ui/Button/Button.tsx
+++ b/packages/ui/components/Button/Button.tsx
@@ -5,7 +5,7 @@ import {
ForwardedRef,
} from 'react'
-import { Logo } from '@dao-dao/dapp/components/Logo'
+import { Logo } from '../Logo'
export interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
children: ReactNode
diff --git a/packages/ui/Button/index.ts b/packages/ui/components/Button/index.ts
similarity index 100%
rename from packages/ui/Button/index.ts
rename to packages/ui/components/Button/index.ts
diff --git a/packages/ui/components/Claims/ClaimsAvaliableCard.tsx b/packages/ui/components/Claims/ClaimsAvaliableCard.tsx
new file mode 100644
index 000000000..4f44ed2aa
--- /dev/null
+++ b/packages/ui/components/Claims/ClaimsAvaliableCard.tsx
@@ -0,0 +1,45 @@
+import { FC } from 'react'
+
+import { TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20'
+
+import { convertMicroDenomToDenomWithDecimals } from '@dao-dao/utils'
+
+import { LogoNoBorder } from '../Logo'
+
+export interface ClaimsAvaliableCardProps {
+ avaliable: number
+ tokenInfo: TokenInfoResponse
+ onClaim: () => void
+ loading: boolean
+}
+
+export const ClaimsAvaliableCard: FC = ({
+ avaliable,
+ tokenInfo,
+ onClaim,
+ loading,
+}) => (
+
+
+ Unclaimed (unstaked ${tokenInfo.symbol})
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {convertMicroDenomToDenomWithDecimals(avaliable, tokenInfo.decimals)}$
+ {tokenInfo.symbol}
+
+ )}
+
+
+ Claim
+
+
+
+)
diff --git a/packages/ui/components/Claims/ClaimsListItem.tsx b/packages/ui/components/Claims/ClaimsListItem.tsx
new file mode 100644
index 000000000..92ebe26dc
--- /dev/null
+++ b/packages/ui/components/Claims/ClaimsListItem.tsx
@@ -0,0 +1,109 @@
+import { FC, useEffect, useState } from 'react'
+
+import { CheckIcon } from '@heroicons/react/outline'
+
+import {
+ convertMicroDenomToDenomWithDecimals,
+ humanReadableDuration,
+ claimAvaliable,
+} from '@dao-dao/utils'
+
+import { BalanceIcon } from '../ContractView/BalanceIcon'
+
+import { Duration } from '@dao-dao/types/contracts/cw3-dao'
+import { Claim, TokenInfoResponse } from '@dao-dao/types/contracts/stake-cw20'
+
+function claimDurationRemaining(claim: Claim, blockHeight: number): Duration {
+ if (claimAvaliable(claim, blockHeight)) {
+ return { time: 0 }
+ }
+ if ('at_height' in claim.release_at) {
+ const releaseBlock = claim.release_at.at_height
+ return { height: releaseBlock - blockHeight }
+ } else if ('at_time' in claim.release_at) {
+ const currentTimeNs = new Date().getTime() * 1000000
+ return {
+ time:
+ (Number(claim.release_at.at_time) - currentTimeNs) / 1000000000 || 0, // To seconds.
+ }
+ }
+
+ // Unreachable.
+ return { time: 0 }
+}
+
+export interface ClaimsListItemProps {
+ claim: Claim
+ unstakingDuration: Duration
+ blockHeight: number
+ tokenInfo: TokenInfoResponse
+ incrementClaimsAvaliable: (n: number) => void
+}
+
+export const ClaimsListItem: FC = ({
+ claim,
+ unstakingDuration,
+ blockHeight,
+ tokenInfo,
+ incrementClaimsAvaliable,
+}: {
+ claim: Claim
+ unstakingDuration: Duration
+ blockHeight: number
+ tokenInfo: TokenInfoResponse
+ incrementClaimsAvaliable: (n: number) => void
+}) => {
+ const avaliable = claimAvaliable(claim, blockHeight)
+
+ const durationForHumans = humanReadableDuration(unstakingDuration)
+ const durationRemaining = claimDurationRemaining(claim, blockHeight)
+
+ // Once the claim expires increment claims avaliable.
+ useEffect(() => {
+ if ('time' in durationRemaining) {
+ const id = setTimeout(
+ () => incrementClaimsAvaliable(Number(claim.amount)),
+ durationRemaining.time * 1000
+ )
+ return () => clearTimeout(id)
+ }
+ }, [claim.amount, durationRemaining, incrementClaimsAvaliable])
+
+ const [durationRemainingForHumans, setDurationRemainingForHumans] = useState(
+ humanReadableDuration(durationRemaining)
+ )
+
+ useEffect(() => {
+ const id = setInterval(() => {
+ setDurationRemainingForHumans((_) =>
+ humanReadableDuration(claimDurationRemaining(claim, blockHeight))
+ )
+ }, 1000)
+ return () => clearInterval(id)
+ }, [claim, blockHeight, setDurationRemainingForHumans])
+
+ return (
+
+
+
+ {convertMicroDenomToDenomWithDecimals(
+ claim.amount,
+ tokenInfo.decimals
+ )}{' '}
+ ${tokenInfo.symbol}
+
+
+ {avaliable ? (
+
+ Avaliable
+
+
+ ) : (
+
+
{durationRemainingForHumans || '0'} left
+
/ {durationForHumans}
+
+ )}
+
+ )
+}
diff --git a/packages/ui/components/Claims/index.ts b/packages/ui/components/Claims/index.ts
new file mode 100644
index 000000000..4eaa05b4e
--- /dev/null
+++ b/packages/ui/components/Claims/index.ts
@@ -0,0 +1,2 @@
+export * from './ClaimsListItem'
+export * from './ClaimsAvaliableCard'
diff --git a/packages/ui/components/ContractView/BalanceCard.tsx b/packages/ui/components/ContractView/BalanceCard.tsx
new file mode 100644
index 000000000..40262b69d
--- /dev/null
+++ b/packages/ui/components/ContractView/BalanceCard.tsx
@@ -0,0 +1,40 @@
+import { FC } from 'react'
+
+import { LogoNoBorder } from '../Logo'
+import { Button } from '../Button'
+import { BalanceIcon } from './BalanceIcon'
+
+export interface BalanceCardProps {
+ denom: string
+ title: string
+ amount: string
+ onManage: () => void
+ loading: boolean
+}
+
+export const BalanceCard: FC = ({
+ denom,
+ title,
+ amount,
+ onManage,
+ loading,
+}) => (
+
+
{title}
+ {loading ? (
+
+
+
+ ) : (
+
+
+ {amount} ${denom}
+
+ )}
+
+
+ Manage
+
+
+
+)
diff --git a/packages/ui/components/ContractView/BalanceIcon.tsx b/packages/ui/components/ContractView/BalanceIcon.tsx
new file mode 100644
index 000000000..c3f59834b
--- /dev/null
+++ b/packages/ui/components/ContractView/BalanceIcon.tsx
@@ -0,0 +1,21 @@
+import { FC } from 'react'
+
+import { useThemeContext } from '../../theme'
+
+export interface BalanceIconProps {
+ iconURI?: string
+}
+
+export const BalanceIcon: FC = ({ iconURI }) => {
+ const { accentColor } = useThemeContext()
+
+ return (
+
+ )
+}
diff --git a/packages/ui/components/ContractView/BalanceListItem.tsx b/packages/ui/components/ContractView/BalanceListItem.tsx
new file mode 100644
index 000000000..3b3a69fc0
--- /dev/null
+++ b/packages/ui/components/ContractView/BalanceListItem.tsx
@@ -0,0 +1,11 @@
+import { FC, ReactNode } from 'react'
+
+export interface BalanceListItemProps {
+ children: ReactNode
+}
+
+export const BalanceListItem: FC = ({ children }) => (
+
+ {children}
+
+)
diff --git a/packages/ui/components/ContractView/ContractHeader.tsx b/packages/ui/components/ContractView/ContractHeader.tsx
new file mode 100644
index 000000000..6de39d674
--- /dev/null
+++ b/packages/ui/components/ContractView/ContractHeader.tsx
@@ -0,0 +1,42 @@
+import { FC } from 'react'
+
+import { EstablishedDate } from './EstablishedDate'
+import { Logo } from '../Logo'
+
+import { HEADER_IMAGES_ENABLED } from '@dao-dao/utils'
+
+export interface ContractHeaderProps {
+ name: string
+ description: string
+ established: Date
+ imgUrl?: string
+}
+
+export const ContractHeader: FC = ({
+ name,
+ description,
+ established,
+ imgUrl,
+}) => (
+
+ {imgUrl && HEADER_IMAGES_ENABLED ? (
+
+ ) : (
+
+ )}
+
+
{name}
+
+
+
+
+)
diff --git a/packages/ui/components/ContractView/EstablishedDate.tsx b/packages/ui/components/ContractView/EstablishedDate.tsx
new file mode 100644
index 000000000..c4de28fd8
--- /dev/null
+++ b/packages/ui/components/ContractView/EstablishedDate.tsx
@@ -0,0 +1,14 @@
+import { FC } from 'react'
+
+export interface EstablishedDateProps {
+ date: Date
+}
+
+export const EstablishedDate: FC = ({ date }) => {
+ const formattedDate = date.toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })
+ return Est. {formattedDate}
+}
diff --git a/packages/ui/components/ContractView/GovInfoListItem.tsx b/packages/ui/components/ContractView/GovInfoListItem.tsx
new file mode 100644
index 000000000..e975f419c
--- /dev/null
+++ b/packages/ui/components/ContractView/GovInfoListItem.tsx
@@ -0,0 +1,20 @@
+import { FC, ReactNode } from 'react'
+
+export interface GovInfoListItemProps {
+ icon: ReactNode
+ text: string
+ value: string
+}
+
+export const GovInfoListItem: FC = ({
+ icon,
+ text,
+ value,
+}) => (
+
+
+ {icon} {text}:
+
+ {value}
+
+)
diff --git a/packages/ui/components/ContractView/GradientHero.tsx b/packages/ui/components/ContractView/GradientHero.tsx
new file mode 100644
index 000000000..9e84a6378
--- /dev/null
+++ b/packages/ui/components/ContractView/GradientHero.tsx
@@ -0,0 +1,31 @@
+import { FC, ReactNode } from 'react'
+
+import { useThemeContext } from '../../theme'
+
+export interface GradientHeroProps {
+ children: ReactNode
+}
+
+export const GradientHero: FC = ({ children }) => {
+ const theme = useThemeContext()
+ const endStop = theme.theme === 'dark' ? '#111213' : '#FFFFFF'
+ const baseRgb = theme.accentColor
+ ? theme.accentColor.split('(')[1].split(')')[0]
+ : '73, 55, 192'
+ return (
+
+ )
+}
diff --git a/packages/ui/components/ContractView/HorizontalInfo.tsx b/packages/ui/components/ContractView/HorizontalInfo.tsx
new file mode 100644
index 000000000..654d9a44b
--- /dev/null
+++ b/packages/ui/components/ContractView/HorizontalInfo.tsx
@@ -0,0 +1,26 @@
+import { Children, FC, ReactNode } from 'react'
+
+export interface HorizontalInfoProps {
+ children: ReactNode
+}
+
+export const HorizontalInfoSection: FC = ({
+ children,
+}) => (
+
+ {children}
+
+)
+
+export const HorizontalInfo: FC = ({ children }) => {
+ const childList = Children.toArray(children)
+ return (
+
+
+ {Children.map(childList, (child) => (
+ {child}
+ ))}
+
+
+ )
+}
diff --git a/packages/ui/components/ContractView/ProposalList.tsx b/packages/ui/components/ContractView/ProposalList.tsx
new file mode 100644
index 000000000..3aac7c0d2
--- /dev/null
+++ b/packages/ui/components/ContractView/ProposalList.tsx
@@ -0,0 +1,34 @@
+import { FC } from 'react'
+
+import Link from 'next/link'
+
+import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao'
+
+import { getProposalEnd, zeroPad } from '@dao-dao/utils'
+
+import { ProposalStatus } from '../ProposalStatus'
+
+export interface ProposalLineProps {
+ proposal: ProposalResponse
+ proposalViewUrl: string
+}
+
+export const ProposalLine: FC = ({
+ proposal,
+ proposalViewUrl,
+}) => (
+
+
+
+
+
# {zeroPad(proposal.id, 6)}
+
+
+
{proposal.title}
+
+ {getProposalEnd(proposal.expires, proposal.status)}
+
+
+
+
+)
diff --git a/packages/ui/components/ContractView/StarButton.tsx b/packages/ui/components/ContractView/StarButton.tsx
new file mode 100644
index 000000000..8425acd27
--- /dev/null
+++ b/packages/ui/components/ContractView/StarButton.tsx
@@ -0,0 +1,33 @@
+import { FC } from 'react'
+
+import { StarIcon as StarOutline } from '@heroicons/react/outline'
+import { StarIcon as StarSolid } from '@heroicons/react/solid'
+
+import { useThemeContext } from '../../theme'
+
+export interface StarButtonProps {
+ pinned: boolean
+ onPin: () => void
+}
+
+export const StarButton: FC = ({ pinned, onPin }) => {
+ const { accentColor } = useThemeContext()
+
+ return (
+ onPin()}
+ style={accentColor ? { color: accentColor } : {}}
+ >
+ {pinned ? (
+
+ ) : (
+
+ )}
+ {pinned ? 'Starred' : 'Star'}
+
+ )
+}
diff --git a/packages/ui/components/ContractView/TreasuryView.tsx b/packages/ui/components/ContractView/TreasuryView.tsx
new file mode 100644
index 000000000..3fa32a164
--- /dev/null
+++ b/packages/ui/components/ContractView/TreasuryView.tsx
@@ -0,0 +1,69 @@
+import { FC } from 'react'
+
+import {
+ nativeTokenLabel,
+ nativeTokenLogoURI,
+ convertMicroDenomToDenomWithDecimals,
+ convertDenomToHumanReadableDenom,
+ NATIVE_DENOM,
+} from '@dao-dao/utils'
+
+import { BalanceListItem } from './BalanceListItem'
+import { BalanceIcon } from './BalanceIcon'
+
+export interface TreasuryBalancesProps {
+ nativeTokens: {
+ denom: string
+ amount: string
+ decimals: number
+ }[]
+ cw20Tokens: {
+ symbol: string
+ amount: string
+ decimals: number
+ }[]
+}
+
+export const TreasuryBalances: FC = ({
+ nativeTokens,
+ cw20Tokens,
+}) => (
+
+ {nativeTokens.map(({ denom, amount, decimals }) => {
+ const symbol = nativeTokenLabel(denom)
+ const icon = nativeTokenLogoURI(denom)
+ return (
+
+
+ {convertMicroDenomToDenomWithDecimals(
+ amount,
+ decimals
+ ).toLocaleString(undefined, {
+ maximumFractionDigits: 20,
+ })}{' '}
+ ${symbol}
+
+ )
+ })}
+ {!nativeTokens.length && (
+
+ 0 $
+ {convertDenomToHumanReadableDenom(NATIVE_DENOM).toUpperCase()}
+
+ )}
+ {cw20Tokens.map(({ symbol, amount, decimals }) => {
+ return (
+
+
+ {convertMicroDenomToDenomWithDecimals(
+ amount,
+ decimals
+ ).toLocaleString(undefined, {
+ maximumFractionDigits: decimals,
+ })}{' '}
+ ${symbol}
+
+ )
+ })}
+
+)
diff --git a/packages/ui/components/ContractView/index.tsx b/packages/ui/components/ContractView/index.tsx
new file mode 100644
index 000000000..ebb82331b
--- /dev/null
+++ b/packages/ui/components/ContractView/index.tsx
@@ -0,0 +1,11 @@
+export * from './ContractHeader'
+export * from './EstablishedDate'
+export * from './GradientHero'
+export * from './HorizontalInfo'
+export * from './StarButton'
+export * from './GovInfoListItem'
+export * from './BalanceCard'
+export * from './BalanceIcon'
+export * from './BalanceListItem'
+export * from './TreasuryView'
+export * from './ProposalList'
diff --git a/apps/dapp/components/CopyToClipboard.tsx b/packages/ui/components/CopyToClipboard.tsx
similarity index 91%
rename from apps/dapp/components/CopyToClipboard.tsx
rename to packages/ui/components/CopyToClipboard.tsx
index 7a1bca5f8..f114be947 100644
--- a/apps/dapp/components/CopyToClipboard.tsx
+++ b/packages/ui/components/CopyToClipboard.tsx
@@ -1,10 +1,9 @@
import { useState } from 'react'
+import { Copy } from '@dao-dao/icons'
+import { useThemeContext } from '@dao-dao/ui'
import { CheckCircleIcon } from '@heroicons/react/outline'
import toast from 'react-hot-toast'
-import { useThemeContext } from 'ui'
-
-import SvgCopy from './icons/Copy'
function concatAddressImpl(
address: string,
@@ -46,7 +45,7 @@ export function CopyToClipboard({
{copied ? (
) : (
-
+
)}
{concatAddress(value, takeN)}
diff --git a/apps/dapp/components/CosmosMessageDisplay.tsx b/packages/ui/components/CosmosMessageDisplay.tsx
similarity index 81%
rename from apps/dapp/components/CosmosMessageDisplay.tsx
rename to packages/ui/components/CosmosMessageDisplay.tsx
index 65d369944..4c11cca21 100644
--- a/apps/dapp/components/CosmosMessageDisplay.tsx
+++ b/packages/ui/components/CosmosMessageDisplay.tsx
@@ -1,8 +1,10 @@
+import { FC } from 'react'
+import { useThemeContext } from '../theme'
+
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material-ocean.css'
import { UnControlled as CodeMirror } from 'react-codemirror2'
-import { useThemeContext } from 'ui'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material.css'
@@ -11,7 +13,13 @@ if (typeof window !== 'undefined' && typeof window.navigator !== 'undefined') {
require('codemirror/mode/javascript/javascript.js')
}
-export const CosmosMessageDisplay = ({ value }: { value: string }) => {
+export interface CosmosMessageDisplayProps {
+ value: string
+}
+
+export const CosmosMessageDisplay: FC = ({
+ value,
+}) => {
const themeCtx = useThemeContext()
const editorTheme = themeCtx.theme !== 'dark' ? 'default' : 'material-ocean'
return (
diff --git a/apps/dapp/components/Execute.tsx b/packages/ui/components/Execute.tsx
similarity index 93%
rename from apps/dapp/components/Execute.tsx
rename to packages/ui/components/Execute.tsx
index ad9d621e6..b6b20f7fa 100644
--- a/apps/dapp/components/Execute.tsx
+++ b/packages/ui/components/Execute.tsx
@@ -1,9 +1,9 @@
import { useState } from 'react'
import { FC } from 'react'
-import { Button } from 'ui'
+import { Button } from './Button'
-import SvgAirplane from './icons/Airplane'
+import { Airplane } from '@dao-dao/icons'
export interface ExecuteProps {
onExecute: () => void
@@ -11,6 +11,8 @@ export interface ExecuteProps {
loading: boolean
}
+export const Hmm: FC<{}> = () => null
+
export const Execute: FC = ({ onExecute, messages, loading }) => {
const [partyMode, setPartMode] = useState(false)
const [partyPhase, setPartyPhase] = useState(1)
@@ -29,7 +31,7 @@ export const Execute: FC = ({ onExecute, messages, loading }) => {
onExecute()}>
- Execute
+ Execute
)}
@@ -93,7 +95,7 @@ export const Execute: FC = ({ onExecute, messages, loading }) => {
loading={loading}
onClick={() => onExecute()}
>
- Execute
+ Execute
diff --git a/apps/dapp/components/GradientWrapper.tsx b/packages/ui/components/GradientWrapper.tsx
similarity index 70%
rename from apps/dapp/components/GradientWrapper.tsx
rename to packages/ui/components/GradientWrapper.tsx
index 0d2bbca50..bc36e74a5 100644
--- a/apps/dapp/components/GradientWrapper.tsx
+++ b/packages/ui/components/GradientWrapper.tsx
@@ -1,10 +1,14 @@
-import { ReactNode } from 'react'
+import { ReactNode, FC } from 'react'
-import { useThemeContext } from 'ui'
+import { useThemeContext } from '../theme'
import { LogoNoBorder } from './Logo'
-export function GradientWrapper({ children }: { children: ReactNode }) {
+export interface GradientWrapperProps {
+ children: ReactNode
+}
+
+export const GradientWrapper: FC = ({ children }) => {
const theme = useThemeContext()
const bg =
theme.theme === 'dark'
@@ -12,10 +16,10 @@ export function GradientWrapper({ children }: { children: ReactNode }) {
: 'url(/gradients/BG-Gradient-Light@2x.png)'
return (
-
+
{CSS.supports('backdrop-filter', 'blur(5px)') && (
diff --git a/apps/dapp/components/LoadingScreen.tsx b/packages/ui/components/LoadingScreen.tsx
similarity index 68%
rename from apps/dapp/components/LoadingScreen.tsx
rename to packages/ui/components/LoadingScreen.tsx
index ed4e0fcc9..fe8c710ee 100644
--- a/apps/dapp/components/LoadingScreen.tsx
+++ b/packages/ui/components/LoadingScreen.tsx
@@ -1,6 +1,6 @@
-import { Logo } from 'components/Logo'
+import { Logo } from './Logo'
-function LoadingScreen() {
+export const LoadingScreen = () => {
return (
@@ -9,5 +9,3 @@ function LoadingScreen() {
)
}
-
-export default LoadingScreen
diff --git a/apps/dapp/components/Logo.tsx b/packages/ui/components/Logo.tsx
similarity index 100%
rename from apps/dapp/components/Logo.tsx
rename to packages/ui/components/Logo.tsx
diff --git a/packages/ui/components/MarkdownPreview.tsx b/packages/ui/components/MarkdownPreview.tsx
new file mode 100644
index 000000000..db8e25d11
--- /dev/null
+++ b/packages/ui/components/MarkdownPreview.tsx
@@ -0,0 +1,12 @@
+import { FC } from 'react'
+import ReactMarkdown from 'react-markdown'
+
+export interface MarkdownPreviewProps {
+ markdown: string
+}
+
+export const MarkdownPreview: FC
= ({ markdown }) => (
+
+ {markdown}
+
+)
diff --git a/packages/ui/components/Modal.tsx b/packages/ui/components/Modal.tsx
new file mode 100644
index 000000000..0ac60c223
--- /dev/null
+++ b/packages/ui/components/Modal.tsx
@@ -0,0 +1,11 @@
+import { ReactNode, FC } from 'react'
+
+export interface ModalProps {
+ children: ReactNode
+}
+
+export const Modal: FC = ({ children }) => (
+
+ {children}
+
+)
diff --git a/apps/dapp/components/Progress.tsx b/packages/ui/components/Progress.tsx
similarity index 100%
rename from apps/dapp/components/Progress.tsx
rename to packages/ui/components/Progress.tsx
diff --git a/packages/ui/components/ProposalDetails/ProposalDetails.tsx b/packages/ui/components/ProposalDetails/ProposalDetails.tsx
new file mode 100644
index 000000000..dd8b394bd
--- /dev/null
+++ b/packages/ui/components/ProposalDetails/ProposalDetails.tsx
@@ -0,0 +1,129 @@
+import { ProposalResponse } from '@dao-dao/types/contracts/cw3-dao'
+import { FC, ReactNode, useState } from 'react'
+import { MarkdownPreview } from '../MarkdownPreview'
+import { Button } from '../Button'
+import { Execute } from '../Execute'
+import { Vote, VoteChoice } from '../Vote'
+import { EyeOffIcon, EyeIcon } from '@heroicons/react/outline'
+import { ProposalMessageTemplateList } from './ProposalMessageTemplateList'
+import { CosmosMessageDisplay } from '../CosmosMessageDisplay'
+import { decodedMessagesString, decodeMessages } from '@dao-dao/utils'
+
+export interface ProposalDetailsProps {
+ proposal: ProposalResponse
+ walletVote: string | undefined
+ walletWeightPercent: number
+ loading: boolean
+ showStaking: boolean
+ setShowStaking: (value: boolean) => void
+ stakingModal?: ReactNode
+ // Transformer to convert a decoded message into a displayable ReactNode. The
+ // caller will likely use this to transform these messages into template
+ // components. Once we have a state package we will want to move
+ // templates into their own package and then this can likely be removed.
+ messageToDisplay: (message: { [key: string]: any }) => ReactNode
+ onExecute: () => void
+ onVote: (choice: VoteChoice) => void
+}
+
+export const ProposalDetails: FC = ({
+ proposal,
+ walletVote,
+ walletWeightPercent,
+ loading,
+ stakingModal,
+ showStaking,
+ setShowStaking,
+ messageToDisplay,
+ onExecute,
+ onVote,
+}) => {
+ const decodedMessages = decodeMessages(proposal.msgs)
+ const [showRaw, setShowRaw] = useState(false)
+
+ return (
+
+
+
{proposal.title}
+
+
+
+
+
Messages
+
+ {decodedMessages?.length ? (
+ showRaw ? (
+
+ ) : (
+
+ )
+ ) : (
+
[]
+ )}
+
+ {!!decodedMessages.length && (
+
+ setShowRaw((s) => !s)}
+ size="sm"
+ variant="secondary"
+ >
+ {showRaw ? (
+ <>
+ Hide raw data
+
+ >
+ ) : (
+ <>
+ Show raw data
+
+ >
+ )}
+
+
+ )}
+ {proposal.status === 'passed' && (
+ <>
+
Status
+
+ >
+ )}
+
Vote
+ {proposal.status === 'open' &&
+ !walletVote &&
+ walletWeightPercent !== 0 && (
+
+ )}
+ {walletVote && (
+
You voted {walletVote} on this proposal.
+ )}
+ {proposal.status !== 'open' && !walletVote && (
+
You did not vote on this proposal.
+ )}
+ {walletWeightPercent === 0 && (
+
+ You must have voting power at the time of proposal creation to vote.{' '}
+ {stakingModal && (
+ setShowStaking(true)}>
+ Stake some tokens?
+
+ )}
+ {stakingModal && showStaking && stakingModal}
+
+ )}
+
+ )
+}
diff --git a/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx b/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx
new file mode 100644
index 000000000..383ab7855
--- /dev/null
+++ b/packages/ui/components/ProposalDetails/ProposalDetailsSidebar.tsx
@@ -0,0 +1,609 @@
+import { FC } from 'react'
+import {
+ ProposalResponse,
+ ProposalTallyResponse,
+} from '@dao-dao/types/contracts/cw3-dao'
+import { ProposalStatus } from '../ProposalStatus'
+import { Tooltip } from '../Tooltip'
+import { Progress } from '../Progress'
+import { CopyToClipboard } from '../CopyToClipboard'
+import { TriangleUp, Abstain } from '@dao-dao/icons'
+import { ExternalLinkIcon, CheckIcon, XIcon } from '@heroicons/react/outline'
+import {
+ CHAIN_TXN_URL_PREFIX,
+ convertMicroDenomToDenomWithDecimals,
+ expirationAtTimeToSecondsFromNow,
+ secondsToWdhms,
+} from '@dao-dao/utils'
+
+// Need this for now as the `WalletVote` enum is stored in dapp state. TODO:
+// remove once we break state into a package.
+export enum WalletVote {
+ Yes = 'yes',
+ No = 'no',
+ Abstain = 'abstain',
+ Veto = 'veto',
+}
+
+export interface ProposalDetailsCardProps {
+ proposal: ProposalResponse
+ memberWhenProposalCreated: boolean
+ walletVote: WalletVote
+ proposalExecutionTXHash: string | undefined
+}
+
+export interface ProposalDetailsVoteStatusProps {
+ proposal: ProposalResponse
+ tally: ProposalTallyResponse
+ tokenDecimals: number
+ // Undefined if max voting period is in blocks.
+ maxVotingSeconds?: number
+ threshold?: {
+ absolute?: number
+ percent: number
+ display: string
+ }
+ quorum?: {
+ percent: number
+ display: string
+ }
+}
+
+export interface YouTooltipProps {
+ label: string
+}
+
+export const YouTooltip: FC = ({ label }) => (
+
+
+ ?
+
+
+)
+
+export const ProposalDetailsCard: FC = ({
+ proposal,
+ memberWhenProposalCreated,
+ walletVote,
+ proposalExecutionTXHash,
+}) => (
+
+
+
+
+ Proposal
+
+
+
+ # {proposal.id.toString().padStart(6, '0')}
+
+
+
+
+
+
+
+
+
+
+
+ You
+
+
+ {!memberWhenProposalCreated ? (
+
+ ) : walletVote === WalletVote.Yes ? (
+
+ Yes
+
+ ) : walletVote === WalletVote.No ? (
+
+ No
+
+ ) : walletVote === WalletVote.Abstain ? (
+
+ Abstain
+
+ ) : walletVote === WalletVote.Veto ? (
+
+ Veto
+
+ ) : walletVote ? (
+
+ Unknown: {walletVote}
+
+ ) : proposal.status === 'open' ? (
+
+ ) : (
+
+ )}
+
+
+
+
Proposer
+
+
+
+ {proposal.status === 'executed' && !proposalExecutionTXHash ? (
+ <>
+
TX
+
Loading...
+ >
+ ) : !!proposalExecutionTXHash ? (
+ <>
+ {CHAIN_TXN_URL_PREFIX ? (
+
+ TX
+
+
+ ) : (
+
TX
+ )}
+
+
+ >
+ ) : null}
+
+
+)
+
+export const ProposalDetailsVoteStatus: FC = ({
+ proposal,
+ tally,
+ tokenDecimals,
+ maxVotingSeconds,
+ threshold,
+ quorum,
+}) => {
+ const localeOptions = { maximumSignificantDigits: 3 }
+
+ const yesVotes = Number(
+ convertMicroDenomToDenomWithDecimals(tally.votes.yes, tokenDecimals)
+ )
+ const noVotes = Number(
+ convertMicroDenomToDenomWithDecimals(tally.votes.no, tokenDecimals)
+ )
+ const abstainVotes = Number(
+ convertMicroDenomToDenomWithDecimals(tally.votes.abstain, tokenDecimals)
+ )
+
+ const totalWeight = Number(
+ convertMicroDenomToDenomWithDecimals(tally.total_weight, tokenDecimals)
+ )
+
+ const turnoutTotal = yesVotes + noVotes + abstainVotes
+ const turnoutYesPercent = turnoutTotal ? (yesVotes / turnoutTotal) * 100 : 0
+ const turnoutNoPercent = turnoutTotal ? (noVotes / turnoutTotal) * 100 : 0
+ const turnoutAbstainPercent = turnoutTotal
+ ? (abstainVotes / turnoutTotal) * 100
+ : 0
+
+ const turnoutPercent = (turnoutTotal / totalWeight) * 100
+ const totalYesPercent = (yesVotes / totalWeight) * 100
+ const totalNoPercent = (noVotes / totalWeight) * 100
+ const totalAbstainPercent = (abstainVotes / totalWeight) * 100
+
+ // When only abstain votes have been cast and there is no quorum,
+ // align the abstain progress bar to the right to line up with Abstain
+ // text.
+ const onlyAbstain = yesVotes === 0 && noVotes === 0 && abstainVotes > 0
+
+ const expiresInSeconds =
+ proposal.expires && 'at_time' in proposal.expires
+ ? expirationAtTimeToSecondsFromNow(proposal.expires)
+ : undefined
+
+ // TODO: Change this wen v1 contracts launch, since the conditions
+ // will change. In v1, all abstain fails instead of passes.
+ const thresholdReached =
+ !!threshold &&
+ yesVotes >=
+ ((quorum ? turnoutTotal : totalWeight) - abstainVotes) *
+ (threshold.percent / 100)
+ const quorumMet = !!quorum && turnoutPercent >= quorum.percent
+
+ const helpfulStatusText =
+ proposal.status === 'open' && threshold && quorum
+ ? thresholdReached && quorumMet
+ ? 'If the current vote stands, this proposal will pass.'
+ : !thresholdReached && quorumMet
+ ? "If the current vote stands, this proposal will fail because insufficient 'Yes' votes have been cast."
+ : thresholdReached && !quorumMet
+ ? 'If the current vote stands, this proposal will fail due to a lack of voter participation.'
+ : undefined
+ : undefined
+ return (
+
+ {helpfulStatusText && (
+
+ {helpfulStatusText}
+
+ )}
+
+ {threshold ? (
+ quorum ? (
+ <>
+
Ratio of votes
+
+
+ {[
+
+ Yes{' '}
+ {turnoutYesPercent.toLocaleString(undefined, localeOptions)}%
+
,
+
+ No {turnoutNoPercent.toLocaleString(undefined, localeOptions)}
+ %
+
,
+ ]
+ .sort(() => yesVotes - noVotes)
+ .map((elem, idx) => (
+
+ {elem}
+
+ ))}
+
+ Abstain{' '}
+ {turnoutAbstainPercent.toLocaleString(undefined, localeOptions)}
+ %
+
+
+
+
+
b.value - a.value),
+ {
+ value: Number(turnoutAbstainPercent),
+ // Secondary is dark with 80% opacity.
+ color: 'rgba(var(--dark), 0.8)',
+ },
+ ],
+ },
+ ]}
+ verticalBars={
+ threshold && [
+ {
+ value: threshold.percent,
+ color: 'rgba(var(--dark), 0.5)',
+ },
+ ]
+ }
+ />
+
+
+
+
90
+ ? 'calc(100% - 32px)'
+ : `calc(${threshold.percent}% - 17px)`,
+ }}
+ width="36px"
+ />
+
+
+
+
+ Passing threshold:{' '}
+ {threshold.display}
+
+
+
+ {thresholdReached ? (
+ <>
+ Passing{' '}
+
+ >
+ ) : (
+ <>
+ Failing{' '}
+
+ >
+ )}
+
+
+
+
+
+
+
+ Turnout
+
+
+
+ {turnoutPercent.toLocaleString(undefined, localeOptions)}%
+
+
+
+
+
+
+
90
+ ? 'calc(100% - 32px)'
+ : `calc(${quorum.percent}% - 17px)`,
+ }}
+ width="36px"
+ />
+
+
+
+
+ Quorum: {quorum.display}
+
+
+
+ {quorumMet ? (
+ <>
+ Reached{' '}
+
+ >
+ ) : (
+ <>
+ Not met{' '}
+
+ >
+ )}
+
+
+
+
+ >
+ ) : (
+ <>
+
+ Turnout
+
+
+
+ {[
+
+ Yes {totalYesPercent.toLocaleString(undefined, localeOptions)}
+ %
+
,
+
+ No {totalNoPercent.toLocaleString(undefined, localeOptions)}%
+
,
+ ]
+ .sort(() => yesVotes - noVotes)
+ .map((elem, idx) => (
+
+ {elem}
+
+ ))}
+
+ Abstain{' '}
+ {totalAbstainPercent.toLocaleString(undefined, localeOptions)}%
+
+
+
+
+
b.value - a.value),
+ {
+ value: Number(totalAbstainPercent),
+ // Secondary is dark with 80% opacity.
+ color: 'rgba(var(--dark), 0.8)',
+ },
+ ],
+ },
+ ]}
+ verticalBars={[
+ {
+ value: threshold.percent,
+ color: 'rgba(var(--dark), 0.5)',
+ },
+ ]}
+ />
+
+
+
+
90
+ ? 'calc(100% - 32px)'
+ : `calc(${threshold.percent}% - 17px)`,
+ }}
+ width="36px"
+ />
+
+
+
+
+ Passing threshold:{' '}
+ {threshold.display}
+
+
+
+ {thresholdReached ? (
+ <>
+ Reached{' '}
+
+ >
+ ) : (
+ <>
+ Not met{' '}
+
+ >
+ )}
+
+
+
+
+ >
+ )
+ ) : null}
+
+ {proposal.status === 'open' &&
+ expiresInSeconds !== undefined &&
+ expiresInSeconds > 0 && (
+ <>
+
+ Time left
+
+
+
+ {secondsToWdhms(expiresInSeconds, 2)}
+
+
+ {maxVotingSeconds !== undefined && (
+
+ )}
+ >
+ )}
+
+ {threshold?.percent === 50 && yesVotes === noVotes && (
+
+
Tie clarification
+
+
{"'Yes' will win a tie vote."}
+
+ )}
+
+ {turnoutTotal > 0 && abstainVotes === turnoutTotal && (
+
+
All abstain clarification
+
+
+ {/* TODO: Change this to fail wen v1 contracts. */}
+ When all abstain, a proposal will pass.
+
+
+ )}
+
+ )
+}
diff --git a/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx b/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx
new file mode 100644
index 000000000..92e43a56c
--- /dev/null
+++ b/packages/ui/components/ProposalDetails/ProposalMessageTemplateList.tsx
@@ -0,0 +1,19 @@
+import { CosmosMsgFor_Empty } from '@dao-dao/types/contracts/cw3-dao'
+import { FC, ReactNode } from 'react'
+import { decodeMessages } from '@dao-dao/utils'
+
+interface ProposalMessageTemplateListProps {
+ msgs: CosmosMsgFor_Empty[]
+ messageToDisplay: (message: { [key: string]: any }) => ReactNode
+}
+
+export const ProposalMessageTemplateList: FC<
+ ProposalMessageTemplateListProps
+> = ({ msgs, messageToDisplay }: ProposalMessageTemplateListProps) => {
+ const components: ReactNode[] = msgs.map((msg, index) => {
+ const decoded = decodeMessages([msg])[0]
+ return {messageToDisplay(decoded)}
+ })
+
+ return <>{components}>
+}
diff --git a/packages/ui/components/ProposalDetails/index.tsx b/packages/ui/components/ProposalDetails/index.tsx
new file mode 100644
index 000000000..9bc49cf7a
--- /dev/null
+++ b/packages/ui/components/ProposalDetails/index.tsx
@@ -0,0 +1,2 @@
+export * from './ProposalDetails'
+export * from './ProposalDetailsSidebar'
diff --git a/apps/dapp/components/ProposalStatus/ProposalStatus.stories.tsx b/packages/ui/components/ProposalStatus/ProposalStatus.stories.tsx
similarity index 100%
rename from apps/dapp/components/ProposalStatus/ProposalStatus.stories.tsx
rename to packages/ui/components/ProposalStatus/ProposalStatus.stories.tsx
diff --git a/apps/dapp/components/ProposalStatus/ProposalStatus.tsx b/packages/ui/components/ProposalStatus/ProposalStatus.tsx
similarity index 84%
rename from apps/dapp/components/ProposalStatus/ProposalStatus.tsx
rename to packages/ui/components/ProposalStatus/ProposalStatus.tsx
index 0919fcd4c..d096e176c 100644
--- a/apps/dapp/components/ProposalStatus/ProposalStatus.tsx
+++ b/packages/ui/components/ProposalStatus/ProposalStatus.tsx
@@ -1,4 +1,4 @@
-import { StatusIcons } from '@components'
+import { StatusIcons } from '../StatusIcons'
type ProposalStatusProps = { status: string }
diff --git a/apps/dapp/components/ProposalStatus/index.ts b/packages/ui/components/ProposalStatus/index.ts
similarity index 100%
rename from apps/dapp/components/ProposalStatus/index.ts
rename to packages/ui/components/ProposalStatus/index.ts
diff --git a/packages/ui/components/StakingModal/ActionButton.tsx b/packages/ui/components/StakingModal/ActionButton.tsx
new file mode 100644
index 000000000..ca09daf0e
--- /dev/null
+++ b/packages/ui/components/StakingModal/ActionButton.tsx
@@ -0,0 +1,24 @@
+import { FC } from 'react'
+import { StakingMode, stakingModeString } from './StakingModal'
+import { Button } from '../Button'
+import { Tooltip } from '../Tooltip'
+
+export interface ActionButtonProps {
+ error: string | undefined
+ loading: boolean
+ mode: StakingMode
+ onClick: () => void
+}
+
+export const ActionButton: FC = ({
+ error,
+ mode,
+ loading,
+ onClick,
+}) => (
+
+
+ {stakingModeString(mode)}
+
+
+)
diff --git a/packages/ui/components/StakingModal/AmountSelector.tsx b/packages/ui/components/StakingModal/AmountSelector.tsx
new file mode 100644
index 000000000..dedbad9b0
--- /dev/null
+++ b/packages/ui/components/StakingModal/AmountSelector.tsx
@@ -0,0 +1,44 @@
+import { ChevronRightIcon, ChevronLeftIcon } from '@heroicons/react/outline'
+
+import { ChangeEvent, FC } from 'react'
+
+export interface AmountSelectorProps {
+ setAmount: (newValue: number) => void
+ amount: number
+ max: number
+}
+
+export const AmountSelector: FC = ({
+ setAmount,
+ amount,
+ max,
+}) => (
+
+ setAmount(amount - 1)}
+ >
+
+
+ ) =>
+ setAmount(e.target.valueAsNumber)
+ }
+ type="number"
+ value={amount}
+ />
+ = max ? 'bg-transparent border border-inactive' : ''
+ }`}
+ disabled={amount + 1 >= max}
+ onClick={() => setAmount(amount + 1)}
+ >
+
+
+
+)
diff --git a/packages/ui/components/StakingModal/ModeButton.tsx b/packages/ui/components/StakingModal/ModeButton.tsx
new file mode 100644
index 000000000..fa7b592fe
--- /dev/null
+++ b/packages/ui/components/StakingModal/ModeButton.tsx
@@ -0,0 +1,24 @@
+import { FC, ReactNode } from 'react'
+
+export interface ModeButtonProps {
+ onClick: () => void
+ active: boolean
+ children: ReactNode
+}
+
+export const ModeButton: FC = ({
+ onClick,
+ active,
+ children,
+}) => (
+
+ {children}
+
+)
diff --git a/packages/ui/components/StakingModal/PercentSelector.tsx b/packages/ui/components/StakingModal/PercentSelector.tsx
new file mode 100644
index 000000000..a775d0583
--- /dev/null
+++ b/packages/ui/components/StakingModal/PercentSelector.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react'
+
+export interface PercentSelectorProps {
+ max: number
+ amount: number
+ tokenDecimals: number
+ setAmount: (newAmount: number) => void
+}
+
+export const PercentSelector: FC = ({
+ max,
+ amount,
+ setAmount,
+ tokenDecimals,
+}) => {
+ const active = (p: number) => max * p === amount
+ const getClassName = (p: number) =>
+ 'rounded-md transition hover:bg-secondary link-text font-normal px-2 py-1' +
+ (active(p) ? ' bg-secondary border border-inactive' : '')
+ const getOnClick = (p: number) => () => {
+ setAmount(Number((p * max).toFixed(tokenDecimals)))
+ }
+
+ return (
+
+
+ 10%
+
+
+ 25%
+
+
+ 50%
+
+
+ 75%
+
+
+ 100%
+
+
+ )
+}
diff --git a/packages/ui/components/StakingModal/StakingModal.tsx b/packages/ui/components/StakingModal/StakingModal.tsx
new file mode 100644
index 000000000..33973f798
--- /dev/null
+++ b/packages/ui/components/StakingModal/StakingModal.tsx
@@ -0,0 +1,257 @@
+import { FC, useState } from 'react'
+import { Modal } from '../Modal'
+import { ModeButton } from './ModeButton'
+import { AmountSelector } from './AmountSelector'
+import { PercentSelector } from './PercentSelector'
+import { ActionButton } from './ActionButton'
+import { Duration } from '@dao-dao/types/contracts/cw3-dao'
+import {
+ durationIsNonZero,
+ humanReadableDuration,
+ convertMicroDenomToDenomWithDecimals,
+} from '@dao-dao/utils'
+
+import { XIcon } from '@heroicons/react/outline'
+
+export enum StakingMode {
+ Stake,
+ Unstake,
+ Claim,
+}
+
+export const stakingModeString = (mode: StakingMode) => {
+ switch (mode) {
+ case StakingMode.Stake:
+ return 'stake'
+ case StakingMode.Unstake:
+ return 'unstake'
+ case StakingMode.Claim:
+ return 'claim'
+ default:
+ return 'internal error'
+ }
+}
+
+export interface StakingModalProps {
+ // The mode to start the staking modal in.
+ defaultMode: StakingMode
+ // The number of tokens in question.
+ amount: number
+ // Sets the number of tokens in question.
+ setAmount: (newAmount: number) => void
+ // Called when the staking modal is closed.
+ onClose: () => void
+ // The number of tokens that are currently claimable.
+ claimableTokens: number
+ // The number of tokens that are unstakable.
+ unstakableTokens: number
+ // The number of tokens that are stakable.
+ stakableTokens: number
+ // The duration for unstaking.
+ unstakingDuration: Duration
+ // Symbol for the token that is being staked.
+ tokenSymbol: string
+ // Decimals for the token that is being staked.
+ tokenDecimals: number
+ // Is there an error?
+ error: string | undefined
+ // Are we ready to stake? Ex: is wallet connected?
+ loading: boolean
+ // Triggered when the stake / unstake / claim button is pressed.
+ onAction: (mode: StakingMode, amount: number) => void
+}
+
+export const StakingModal: FC = ({
+ defaultMode,
+ amount,
+ setAmount,
+ onClose,
+ claimableTokens,
+ stakableTokens,
+ unstakableTokens,
+ unstakingDuration,
+ tokenSymbol,
+ tokenDecimals,
+ loading,
+ error,
+ onAction,
+}) => {
+ const [mode, setMode] = useState(defaultMode)
+ const canClaim = !!claimableTokens
+
+ const maxTx = mode === StakingMode.Stake ? stakableTokens : unstakableTokens
+
+ const invalidAmount = (): string | undefined => {
+ if (mode === StakingMode.Claim) {
+ return claimableTokens > 0 ? undefined : "Can't claim zero tokens"
+ }
+ if (amount <= 0) {
+ return `Can't ${stakingModeString(mode)} zero tokens.`
+ }
+ if (amount > maxTx) {
+ return "Can't ${stakingModeString(mode)} more tokens than you own."
+ }
+ return undefined
+ }
+ error = error || invalidAmount()
+
+ return (
+
+
+
+
+
+
+
+
Manage staking
+
+
+
+ setMode(StakingMode.Stake)}
+ >
+ Stake
+
+ setMode(StakingMode.Unstake)}
+ >
+ Unstake
+
+ {canClaim && (
+ setMode(StakingMode.Claim)}
+ >
+ Claim
+
+ )}
+
+ {mode === StakingMode.Stake && (
+
setAmount(amount)}
+ max={stakableTokens}
+ tokenDecimals={tokenDecimals}
+ />
+ )}
+ {mode === StakingMode.Unstake && (
+ setAmount(amount)}
+ max={unstakableTokens}
+ unstakingDuration={unstakingDuration}
+ tokenDecimals={tokenDecimals}
+ />
+ )}
+ {mode === StakingMode.Claim && (
+
+ )}
+
+
+ onAction(
+ mode,
+ mode === StakingMode.Claim ? claimableTokens : amount
+ )
+ }
+ />
+
+
+
+ )
+}
+
+interface AmountSelectionProps {
+ amount: number
+ max: number
+ setAmount: (newAmount: number) => void
+ tokenDecimals: number
+}
+
+const AmountSelectionBody: FC = ({
+ amount,
+ setAmount,
+ max,
+ tokenDecimals,
+}) => (
+
+
Choose your token amount
+
+ {amount > max && (
+
+ Can{"'"}t stake more tokens than you own.
+
+ )}
+
Max available {max}
+
+
+)
+
+const UnstakingModeBody: FC<
+ AmountSelectionProps & { unstakingDuration: Duration }
+> = ({ amount, setAmount, max, unstakingDuration, tokenDecimals }) => (
+ <>
+
+ {durationIsNonZero(unstakingDuration) && (
+ <>
+
+
+
+ Unstaking period: {humanReadableDuration(unstakingDuration)}
+
+
+ There will be {humanReadableDuration(unstakingDuration)} between the
+ time you decide to unstake your tokens and the time you can redeem
+ them.
+
+
+ >
+ )}
+ >
+)
+
+interface ClaimBodyProps {
+ amount: number
+ tokenDecimals: number
+ tokenSymbol: string
+}
+
+const ClaimBody: FC = ({
+ amount,
+ tokenSymbol,
+ tokenDecimals,
+}) => (
+
+
+ {convertMicroDenomToDenomWithDecimals(amount, tokenDecimals)} $
+ {tokenSymbol} avaliable
+
+
+ Claim them to receive your unstaked tokens.
+
+
+)
diff --git a/packages/ui/components/StakingModal/index.tsx b/packages/ui/components/StakingModal/index.tsx
new file mode 100644
index 000000000..92f4237c9
--- /dev/null
+++ b/packages/ui/components/StakingModal/index.tsx
@@ -0,0 +1 @@
+export { StakingMode, StakingModal } from './StakingModal'
diff --git a/packages/ui/components/StatusIcons/StatusIcons.tsx b/packages/ui/components/StatusIcons/StatusIcons.tsx
new file mode 100644
index 000000000..4e77cb0a2
--- /dev/null
+++ b/packages/ui/components/StatusIcons/StatusIcons.tsx
@@ -0,0 +1,8 @@
+import { Executed, Open, Passed, Rejected } from '@dao-dao/icons'
+
+export const StatusIcons: { [key: string]: JSX.Element } = {
+ open: ,
+ executed: ,
+ passed: ,
+ rejected: ,
+}
diff --git a/apps/dapp/components/StatusIcons/index.ts b/packages/ui/components/StatusIcons/index.ts
similarity index 100%
rename from apps/dapp/components/StatusIcons/index.ts
rename to packages/ui/components/StatusIcons/index.ts
diff --git a/packages/ui/components/Tooltip.tsx b/packages/ui/components/Tooltip.tsx
new file mode 100644
index 000000000..897dcf631
--- /dev/null
+++ b/packages/ui/components/Tooltip.tsx
@@ -0,0 +1,15 @@
+import { FC, ReactNode } from 'react'
+
+import ReachTooltip from '@reach/tooltip'
+
+export interface TooltipProps {
+ label: ReactNode | undefined
+ children: ReactNode
+}
+
+export const Tooltip: FC = ({ label, children }) =>
+ !!label ? (
+ {children}
+ ) : (
+ <>{children}>
+ )
diff --git a/apps/dapp/components/TooltipsDisplay/TooltipIcon.tsx b/packages/ui/components/TooltipIcon.tsx
similarity index 100%
rename from apps/dapp/components/TooltipsDisplay/TooltipIcon.tsx
rename to packages/ui/components/TooltipIcon.tsx
diff --git a/apps/dapp/components/Vote.tsx b/packages/ui/components/Vote.tsx
similarity index 92%
rename from apps/dapp/components/Vote.tsx
rename to packages/ui/components/Vote.tsx
index bd933d706..3cc542d91 100644
--- a/apps/dapp/components/Vote.tsx
+++ b/packages/ui/components/Vote.tsx
@@ -1,10 +1,9 @@
import { FC, useState } from 'react'
+import { Button } from '@dao-dao/ui'
import { CheckIcon, XIcon } from '@heroicons/react/outline'
-import { Button } from 'ui'
-import SvgAbstain from './icons/Abstain'
-import SvgAirplane from './icons/Airplane'
+import { Abstain, Airplane } from '@dao-dao/icons'
export enum VoteChoice {
Yes,
@@ -64,7 +63,7 @@ export const Vote: FC = ({ onVote, voterWeight, loading }) => {
}
variant="secondary"
>
-
+
Abstain
= ({ onVote, voterWeight, loading }) => {
loading={loading}
onClick={() => onVote(selected as VoteChoice)}
>
- Vote
+ Vote
)
diff --git a/packages/ui/components/WalletConnect.tsx b/packages/ui/components/WalletConnect.tsx
new file mode 100644
index 000000000..dcab6cbfd
--- /dev/null
+++ b/packages/ui/components/WalletConnect.tsx
@@ -0,0 +1,89 @@
+import { FC, useState } from 'react'
+
+import { Wallet, Copy } from '@dao-dao/icons'
+import { CheckCircleIcon, LogoutIcon } from '@heroicons/react/outline'
+import { Button } from './Button'
+import { Tooltip } from './Tooltip'
+
+export interface WalletConnectProps {
+ walletAddress: string
+ walletName: string | undefined
+ walletBalance: number
+ walletBalanceDenom: string
+ handleConnect: () => void
+}
+
+export const WalletConnect: FC
= ({
+ walletAddress,
+ walletName,
+ walletBalance,
+ walletBalanceDenom,
+ handleConnect,
+}) =>
+ walletAddress ? (
+
+
+
+
+ {walletName}
+
+
+ {walletBalance} {walletBalanceDenom}
+
+
+
+
+
+
+
+
+ ) : (
+
+ )
+
+interface CopyButtonProps {
+ text: string
+}
+
+const CopyButton: FC = ({ text }) => {
+ const [copied, setCopied] = useState(false)
+ return (
+
+ {
+ navigator.clipboard.writeText(text)
+ setTimeout(() => setCopied(false), 2000)
+ setCopied(true)
+ }}
+ type="button"
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+interface DisconnectButtonProps {
+ onClick: () => void
+}
+
+const DisconnectButton: FC = ({ onClick }) => (
+
+
+
+
+
+)
diff --git a/apps/dapp/components/input/AddressInput.tsx b/packages/ui/components/input/AddressInput.tsx
similarity index 91%
rename from apps/dapp/components/input/AddressInput.tsx
rename to packages/ui/components/input/AddressInput.tsx
index 6360054ca..58807b622 100644
--- a/apps/dapp/components/input/AddressInput.tsx
+++ b/packages/ui/components/input/AddressInput.tsx
@@ -8,7 +8,7 @@ import {
Validate,
} from 'react-hook-form'
-import SvgWallet from '@components/icons/Wallet'
+import { Wallet } from '@dao-dao/icons'
export function AddressInput>({
label,
@@ -34,7 +34,7 @@ export function AddressInput>({
className={`flex items-center gap-1 bg-transparent rounded-lg px-3 py-2 transition focus-within:ring-1 focus-within:outline-none ring-brand ring-offset-0 border-default border border-default text-sm font-mono
${error ? ' ring-error ring-1' : ''}`}
>
-
+
diff --git a/apps/dapp/components/input/InputErrorMessage.tsx b/packages/ui/components/input/InputErrorMessage.tsx
similarity index 100%
rename from apps/dapp/components/input/InputErrorMessage.tsx
rename to packages/ui/components/input/InputErrorMessage.tsx
diff --git a/apps/dapp/components/input/InputLabel.tsx b/packages/ui/components/input/InputLabel.tsx
similarity index 91%
rename from apps/dapp/components/input/InputLabel.tsx
rename to packages/ui/components/input/InputLabel.tsx
index c3b1094c7..2aa3d3675 100644
--- a/apps/dapp/components/input/InputLabel.tsx
+++ b/packages/ui/components/input/InputLabel.tsx
@@ -2,7 +2,7 @@ import { ComponentProps, ReactNode } from 'react'
import clsx from 'clsx'
-import { TooltipIcon } from '@components/TooltipsDisplay/TooltipIcon'
+import { TooltipIcon } from '../TooltipIcon'
export type InputLabelProps = Omit
, 'children'> & {
mono?: boolean
diff --git a/apps/dapp/components/input/NumberInput.tsx b/packages/ui/components/input/NumberInput.tsx
similarity index 100%
rename from apps/dapp/components/input/NumberInput.tsx
rename to packages/ui/components/input/NumberInput.tsx
diff --git a/apps/dapp/components/input/SelectInput.tsx b/packages/ui/components/input/SelectInput.tsx
similarity index 100%
rename from apps/dapp/components/input/SelectInput.tsx
rename to packages/ui/components/input/SelectInput.tsx
diff --git a/apps/dapp/components/input/TextAreaInput.tsx b/packages/ui/components/input/TextAreaInput.tsx
similarity index 97%
rename from apps/dapp/components/input/TextAreaInput.tsx
rename to packages/ui/components/input/TextAreaInput.tsx
index 635b6b447..59b73b0eb 100644
--- a/apps/dapp/components/input/TextAreaInput.tsx
+++ b/packages/ui/components/input/TextAreaInput.tsx
@@ -14,7 +14,6 @@ export function TextareaInput<
register,
error,
validation,
- border = true,
}: {
label: FieldName
register: UseFormRegister
diff --git a/apps/dapp/components/input/TextInput.tsx b/packages/ui/components/input/TextInput.tsx
similarity index 100%
rename from apps/dapp/components/input/TextInput.tsx
rename to packages/ui/components/input/TextInput.tsx
diff --git a/apps/dapp/components/input/ToggleInput.tsx b/packages/ui/components/input/ToggleInput.tsx
similarity index 100%
rename from apps/dapp/components/input/ToggleInput.tsx
rename to packages/ui/components/input/ToggleInput.tsx
diff --git a/apps/dapp/components/input/TokenAmountInput.tsx b/packages/ui/components/input/TokenAmountInput.tsx
similarity index 100%
rename from apps/dapp/components/input/TokenAmountInput.tsx
rename to packages/ui/components/input/TokenAmountInput.tsx
diff --git a/packages/ui/components/input/index.tsx b/packages/ui/components/input/index.tsx
new file mode 100644
index 000000000..965b3dc90
--- /dev/null
+++ b/packages/ui/components/input/index.tsx
@@ -0,0 +1,11 @@
+export * from './AddressInput'
+export * from './CodeMirrorInput'
+export * from './ImageSelector'
+export * from './InputErrorMessage'
+export * from './InputLabel'
+export * from './NumberInput'
+export * from './SelectInput'
+export * from './TextAreaInput'
+export * from './TextInput'
+export * from './ToggleInput'
+export * from './TokenAmountInput'
diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx
index bf3c22fc2..014f579a8 100644
--- a/packages/ui/index.tsx
+++ b/packages/ui/index.tsx
@@ -1,3 +1,22 @@
-import * as React from 'react'
-export * from './Button'
+export * from './components/Button/index'
export * from './theme'
+export * from './components/StakingModal'
+export * from './components/Modal'
+export * from './components/Tooltip'
+export * from './components/ProposalDetails'
+export * from './components/MarkdownPreview'
+export * from './components/CosmosMessageDisplay'
+export * from './components/input/index'
+export * from './components/Execute'
+export * from './components/Vote'
+export * from './components/WalletConnect'
+export * from './components/Progress'
+export * from './components/ProposalStatus'
+export * from './components/CopyToClipboard'
+export * from './components/TooltipIcon'
+export * from './components/Logo'
+export * from './components/ContractView'
+export * from './components/GradientWrapper'
+export * from './components/LoadingScreen'
+export * from './components/Breadcrumbs'
+export * from './components/Claims'
diff --git a/packages/ui/package.json b/packages/ui/package.json
index d1028c79e..6d3063772 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,5 +1,5 @@
{
- "name": "ui",
+ "name": "@dao-dao/ui",
"version": "0.0.0",
"main": "./index.tsx",
"types": "./index.tsx",
@@ -13,6 +13,16 @@
"storybook": "start-storybook --ci -p 6006 --no-manager-cache",
"build-storybook": "build-storybook"
},
+ "dependencies": {
+ "@dao-dao/utils": "*",
+ "@dao-dao/icons": "*",
+ "@heroicons/react": "^1.0.5",
+ "@dao-dao/types": "^0.0.8",
+ "react-markdown": "^8.0.0",
+ "react-hook-form": "^7.20.4",
+ "clsx": "^1.1.1",
+ "codemirror": "^5.65.0"
+ },
"devDependencies": {
"@babel/core": "^7.17.5",
"@dao-dao/config": "*",
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
index cd6c94d6e..b702b31a9 100644
--- a/packages/ui/tsconfig.json
+++ b/packages/ui/tsconfig.json
@@ -1,5 +1,6 @@
{
- "extends": "tsconfig/react-library.json",
+ "extends": "tsconfig/nextjs.json",
"include": ["."],
- "exclude": ["dist", "build", "node_modules"]
+ "exclude": ["dist", "build", "node_modules"],
+ "moduleResolution": "node"
}
diff --git a/packages/utils/claims.ts b/packages/utils/claims.ts
new file mode 100644
index 000000000..d77c36a9d
--- /dev/null
+++ b/packages/utils/claims.ts
@@ -0,0 +1,13 @@
+import { Claim } from '@dao-dao/types/contracts/stake-cw20'
+
+export function claimAvaliable(claim: Claim, blockHeight: number) {
+ if ('at_height' in claim.release_at) {
+ return blockHeight >= claim.release_at.at_height
+ } else if ('at_time' in claim.release_at) {
+ const currentTimeNs = new Date().getTime() * 1000000
+ return currentTimeNs >= Number(claim.release_at.at_time)
+ }
+
+ // Unreachable.
+ return true
+}
diff --git a/apps/dapp/util/constants.ts b/packages/utils/constants.ts
similarity index 100%
rename from apps/dapp/util/constants.ts
rename to packages/utils/constants.ts
diff --git a/apps/dapp/util/conversion.ts b/packages/utils/conversion.ts
similarity index 58%
rename from apps/dapp/util/conversion.ts
rename to packages/utils/conversion.ts
index 512084f3e..2b183723b 100644
--- a/apps/dapp/util/conversion.ts
+++ b/packages/utils/conversion.ts
@@ -1,13 +1,11 @@
import {
- Duration,
Expiration,
+ Status,
Threshold as DaoThreshold,
ThresholdResponse,
} from '@dao-dao/types/contracts/cw3-dao'
import { Threshold as SigThreshold } from '@dao-dao/types/contracts/cw3-multisig'
-
-import { NATIVE_DECIMALS, NATIVE_DENOM } from './constants'
-import ibcAssets from './ibc_assets.json'
+import { secondsToWdhms } from './time'
export function convertMicroDenomToDenomWithDecimals(
amount: number | string,
@@ -27,23 +25,11 @@ export function convertDenomToMicroDenomWithDecimals(
if (typeof amount === 'string') {
amount = Number(amount)
}
- amount = amount * Math.pow(10, decimals)
+ // Need to round. Example: `8.029409 * Math.pow(10, 6)`.
+ amount = Math.round(amount * Math.pow(10, decimals))
return isNaN(amount) ? '0' : String(amount)
}
-export function convertFromMicroDenom(denom: string) {
- return denom?.substring(1).toUpperCase()
-}
-
-export function convertToFixedDecimals(amount: number | string): string {
- if (typeof amount === 'string') {
- amount = Number(amount)
- }
- if (amount > 0.01) {
- return amount.toFixed(2)
- } else return String(amount)
-}
-
export function convertDenomToHumanReadableDenom(denom: string): string {
if (denom.startsWith('u')) {
return denom.substring(1)
@@ -58,14 +44,17 @@ export function convertDenomToContractReadableDenom(denom: string): string {
return 'u' + denom
}
-export const zeroVotingCoin = {
- amount: '0',
- denom: 'ucredits',
+export function convertFromMicroDenom(denom: string) {
+ return denom?.substring(1).toUpperCase()
}
-export const zeroStakingCoin = {
- amount: '0',
- denom: process.env.NEXT_PUBLIC_STAKING_DENOM || 'ujuno',
+export function convertToFixedDecimals(amount: number | string): string {
+ if (typeof amount === 'string') {
+ amount = Number(amount)
+ }
+ if (amount > 0.01) {
+ return amount.toFixed(2)
+ } else return String(amount)
}
export const getDaoThresholdAndQuorum = (
@@ -134,73 +123,6 @@ export const thresholdString = (
}
}
-export function humanReadableDuration(d: Duration) {
- if ('height' in d) {
- return `${d.height} blocks`
- }
- if (d.time == 0) {
- return '0 seconds'
- }
- return `${secondsToWdhms(d.time.toString())}`
-}
-
-const secPerDay = 24 * 60 * 60
-export function secondsToWdhms(seconds: string | number, numUnits = 5): string {
- const secondsInt = Number(seconds)
- const w = Math.floor(secondsInt / (secPerDay * 7))
- const d = Math.floor((secondsInt % (secPerDay * 7)) / secPerDay)
- const h = Math.floor((secondsInt % secPerDay) / 3600)
- const m = Math.floor((secondsInt % 3600) / 60)
- const s = Math.floor(secondsInt % 60)
-
- const wDisplay = w ? w + (w === 1 ? ' wk' : ' wks') : null
- const dDisplay = d ? d + (d === 1 ? ' day' : ' days') : null
- const hDisplay = h ? h + (h === 1 ? ' hr' : ' hrs') : null
- const mDisplay = m ? m + (m === 1 ? ' min' : ' mins') : null
- const sDisplay = s ? s + (s === 1 ? ' sec' : ' secs') : null
-
- return (
- [wDisplay, dDisplay, hDisplay, mDisplay, sDisplay]
- // Ignore empty values.
- .filter(Boolean)
- // Only keep certain precision of units.
- .slice(0, numUnits)
- // Separate with commas.
- .join(', ')
- )
-}
-
-export function nativeTokenLabel(denom: string): string {
- // Search IBC asset strings (junoDenom) if denom is in IBC format.
- // Otherwise just check microdenoms.
- const asset = denom.startsWith('ibc')
- ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
- : ibcAssets.tokens.find(({ denom: d }) => d === denom)
- // If no asset, assume it's already a microdenom.
- return asset?.symbol || convertFromMicroDenom(denom)
-}
-
-export function nativeTokenLogoURI(denom: string): string | undefined {
- if (denom === 'ujuno' || denom == 'ujunox') {
- return '/juno-symbol.png'
- }
-
- const asset = denom.startsWith('ibc')
- ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
- : ibcAssets.tokens.find(({ denom: d }) => d === denom)
- return asset?.logoURI
-}
-
-export function nativeTokenDecimals(denom: string): number | undefined {
- if (denom === NATIVE_DENOM) {
- return NATIVE_DECIMALS
- }
- const asset = denom.startsWith('ibc')
- ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
- : ibcAssets.tokens.find(({ denom: d }) => d === denom)
- return asset?.decimals
-}
-
export const expirationAtTimeToSecondsFromNow = (exp: Expiration) => {
if (!('at_time' in exp)) {
return undefined
@@ -212,3 +134,33 @@ export const expirationAtTimeToSecondsFromNow = (exp: Expiration) => {
return endSeconds - nowSeconds
}
+
+export const zeroPad = (num: number, target: number) => {
+ const s = num.toString()
+ if (s.length > target) {
+ return s
+ }
+ return '0'.repeat(target - s.length) + s
+}
+
+export const getProposalEnd = (exp: Expiration, status: Status) => {
+ if (status != 'open' && status != 'pending') {
+ return 'Completed'
+ }
+ if (exp && 'at_time' in exp) {
+ const secondsFromNow = expirationAtTimeToSecondsFromNow(exp)
+ // Type check, but should never happen.
+ if (secondsFromNow === undefined) {
+ return ''
+ }
+
+ if (secondsFromNow <= 0) {
+ return 'Completed'
+ } else {
+ return secondsToWdhms(secondsFromNow)
+ }
+ }
+ // Not much we can say about proposals that expire at a block
+ // height / never.
+ return ''
+}
diff --git a/packages/utils/duration.ts b/packages/utils/duration.ts
new file mode 100644
index 000000000..187d795ac
--- /dev/null
+++ b/packages/utils/duration.ts
@@ -0,0 +1,19 @@
+import { Duration } from '@dao-dao/types/contracts/cw3-dao'
+import { secondsToWdhms } from './time'
+
+export const durationIsNonZero = (d: Duration) => {
+ if ('height' in d) {
+ return d.height !== 0
+ }
+ return d.time !== 0
+}
+
+export const humanReadableDuration = (d: Duration) => {
+ if ('height' in d) {
+ return `${d.height} blocks`
+ }
+ if (d.time == 0) {
+ return '0 seconds'
+ }
+ return `${secondsToWdhms(d.time.toString())}`
+}
diff --git a/packages/utils/ibc.ts b/packages/utils/ibc.ts
new file mode 100644
index 000000000..d7d52f729
--- /dev/null
+++ b/packages/utils/ibc.ts
@@ -0,0 +1,34 @@
+import ibcAssets from './ibc_assets.json'
+import { convertFromMicroDenom } from './conversion'
+import { NATIVE_DENOM, NATIVE_DECIMALS } from './constants'
+
+export function nativeTokenLabel(denom: string): string {
+ // Search IBC asset strings (junoDenom) if denom is in IBC format.
+ // Otherwise just check microdenoms.
+ const asset = denom.startsWith('ibc')
+ ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
+ : ibcAssets.tokens.find(({ denom: d }) => d === denom)
+ // If no asset, assume it's already a microdenom.
+ return asset?.symbol || convertFromMicroDenom(denom)
+}
+
+export function nativeTokenLogoURI(denom: string): string | undefined {
+ if (denom === 'ujuno' || denom == 'ujunox') {
+ return '/juno-symbol.png'
+ }
+
+ const asset = denom.startsWith('ibc')
+ ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
+ : ibcAssets.tokens.find(({ denom: d }) => d === denom)
+ return asset?.logoURI
+}
+
+export function nativeTokenDecimals(denom: string): number | undefined {
+ if (denom === NATIVE_DENOM) {
+ return NATIVE_DECIMALS
+ }
+ const asset = denom.startsWith('ibc')
+ ? ibcAssets.tokens.find(({ junoDenom }) => junoDenom === denom)
+ : ibcAssets.tokens.find(({ denom: d }) => d === denom)
+ return asset?.decimals
+}
diff --git a/apps/dapp/util/ibc_assets.json b/packages/utils/ibc_assets.json
similarity index 100%
rename from apps/dapp/util/ibc_assets.json
rename to packages/utils/ibc_assets.json
diff --git a/packages/utils/index.tsx b/packages/utils/index.tsx
index cb877561a..31a1eda0e 100644
--- a/packages/utils/index.tsx
+++ b/packages/utils/index.tsx
@@ -1 +1,8 @@
export * from './isValidAddress'
+export * from './duration'
+export * from './time'
+export * from './conversion'
+export * from './ibc'
+export * from './constants'
+export * from './messages'
+export * from './claims'
diff --git a/packages/utils/messages.ts b/packages/utils/messages.ts
new file mode 100644
index 000000000..cfbec4f60
--- /dev/null
+++ b/packages/utils/messages.ts
@@ -0,0 +1,102 @@
+import {
+ CosmosMsgFor_Empty,
+ WasmMsg,
+ ProposalResponse,
+} from '@dao-dao/types/contracts/cw3-dao'
+
+export function parseEncodedMessage(base64String?: string) {
+ if (base64String) {
+ const jsonMessage = decodeURIComponent(escape(atob(base64String)))
+ if (jsonMessage) {
+ return JSON.parse(jsonMessage)
+ }
+ }
+ return undefined
+}
+
+export type WasmMsgType =
+ | 'execute'
+ | 'instantiate'
+ | 'migrate'
+ | 'update_admin'
+ | 'clear_admin'
+
+const WASM_TYPES: WasmMsgType[] = [
+ 'execute',
+ 'instantiate',
+ 'migrate',
+ 'update_admin',
+ 'clear_admin',
+]
+
+const BINARY_WASM_TYPES: { [key: string]: boolean } = {
+ execute: true,
+ instantiate: true,
+ migrate: true,
+}
+
+export function isWasmMsg(msg?: CosmosMsgFor_Empty): msg is { wasm: WasmMsg } {
+ if (msg) {
+ return (msg as any).wasm !== undefined
+ }
+ return false
+}
+
+function getWasmMsgType(wasm: WasmMsg): WasmMsgType | undefined {
+ for (const wasmType of WASM_TYPES) {
+ if (!!(wasm as any)[wasmType]) {
+ return wasmType
+ }
+ }
+ return undefined
+}
+
+function isBinaryType(msgType?: WasmMsgType): boolean {
+ if (msgType) {
+ return !!BINARY_WASM_TYPES[msgType]
+ }
+ return false
+}
+
+export function decodeMessages(
+ msgs: ProposalResponse['msgs']
+): { [key: string]: any }[] {
+ const decodedMessageArray: any[] = []
+ const proposalMsgs = Object.values(msgs)
+ for (const msgObj of proposalMsgs) {
+ if (isWasmMsg(msgObj)) {
+ const msgType = getWasmMsgType(msgObj.wasm)
+ if (msgType && isBinaryType(msgType)) {
+ const base64Msg = (msgObj.wasm as any)[msgType]
+ if (base64Msg) {
+ const msg = parseEncodedMessage(base64Msg.msg)
+ if (msg) {
+ decodedMessageArray.push({
+ ...msgObj,
+ wasm: {
+ ...msgObj.wasm,
+ [msgType]: {
+ ...base64Msg,
+ msg,
+ },
+ },
+ })
+ }
+ }
+ }
+ } else {
+ decodedMessageArray.push(msgObj)
+ }
+ }
+
+ const decodedMessages = decodedMessageArray.length
+ ? decodedMessageArray
+ : proposalMsgs
+
+ return decodedMessages
+}
+
+export function decodedMessagesString(msgs: ProposalResponse['msgs']): string {
+ const decodedMessageArray = decodeMessages(msgs)
+ return JSON.stringify(decodedMessageArray, undefined, 2)
+}
diff --git a/packages/utils/time.ts b/packages/utils/time.ts
new file mode 100644
index 000000000..74f4184a1
--- /dev/null
+++ b/packages/utils/time.ts
@@ -0,0 +1,28 @@
+const secPerDay = 24 * 60 * 60
+export const secondsToWdhms = (
+ seconds: string | number,
+ numUnits = 5
+): string => {
+ const secondsInt = Number(seconds)
+ const w = Math.floor(secondsInt / (secPerDay * 7))
+ const d = Math.floor((secondsInt % (secPerDay * 7)) / secPerDay)
+ const h = Math.floor((secondsInt % secPerDay) / 3600)
+ const m = Math.floor((secondsInt % 3600) / 60)
+ const s = Math.floor(secondsInt % 60)
+
+ const wDisplay = w ? w + (w === 1 ? ' wk' : ' wks') : null
+ const dDisplay = d ? d + (d === 1 ? ' day' : ' days') : null
+ const hDisplay = h ? h + (h === 1 ? ' hr' : ' hrs') : null
+ const mDisplay = m ? m + (m === 1 ? ' min' : ' mins') : null
+ const sDisplay = s ? s + (s === 1 ? ' sec' : ' secs') : null
+
+ return (
+ [wDisplay, dDisplay, hDisplay, mDisplay, sDisplay]
+ // Ignore empty values.
+ .filter(Boolean)
+ // Only keep certain precision of units.
+ .slice(0, numUnits)
+ // Separate with commas.
+ .join(', ')
+ )
+}