import { Add } from '@mui/icons-material' import clsx from 'clsx' import { ComponentType, Fragment, useRef, useState } from 'react' import { CSVLink } from 'react-csv' import { useTranslation } from 'react-i18next' import { HugeDecimal } from '@dao-dao/math' import { ButtonLinkProps, LoadingDataWithError, StatefulDaoMemberCardProps, StatefulEntityDisplayProps, TypedOption, } from '@dao-dao/types' import { useQuerySyncedState } from '../../../hooks' import { Button } from '../../buttons' import { ErrorPage } from '../../error' import { GridCardContainer } from '../../GridCardContainer' import { Dropdown } from '../../inputs/Dropdown' import { Loader } from '../../logo' import { PAGINATION_MIN_PAGE, Pagination } from '../../Pagination' import { Tooltip } from '../../tooltip' import { TooltipInfoIcon } from '../../tooltip/TooltipInfoIcon' import { VotingPowerDistribution } from '../../VotingPowerDistribution' export interface MembersTabProps { DaoMemberCard: ComponentType members: LoadingDataWithError /** * URL to add a new member. Probably a prefilled proposal URL. */ addMemberHref?: string /** * If the add member URL is set, then the button will be shown. If not a * member, the button will disabled but still visible. */ isMember?: boolean ButtonLink: ComponentType // If defined, will show the top voter distribution. topVoters: | { show: true EntityDisplay: ComponentType } | { show: false } } // Store absolutes as negative numbers and percentages as positive numbers. enum TopStakerState { TenAbsolute = -10, TenPercent = 10, TwentyPercent = 20, All = 100, } const MEMBERS_PER_PAGE = 100 export const MembersTab = ({ DaoMemberCard, members, isMember, addMemberHref, ButtonLink, topVoters, }: MembersTabProps) => { const { t } = useTranslation() const csvLinkRef = useRef() const [membersPage, setMembersPage] = useQuerySyncedState({ param: 'mp', defaultValue: PAGINATION_MIN_PAGE, }) const [topStakerState, setTopStakerState] = useState( TopStakerState.TenAbsolute ) const topStakerStateOptions: TypedOption[] = [ { label: t('title.topAbsolute', { count: -TopStakerState.TenAbsolute }), value: TopStakerState.TenAbsolute, }, { label: t('title.topPercent', { percent: TopStakerState.TenPercent }), value: TopStakerState.TenPercent, }, { label: t('title.topPercent', { percent: TopStakerState.TwentyPercent }), value: TopStakerState.TwentyPercent, }, { label: t('title.all'), value: TopStakerState.All, }, ] const topStakerStateOption = topStakerStateOptions.find( (option) => option.value === topStakerState ) // If anyone's voting power is still loading, can't yet show the top members. const sortedMembers = members.loading || members.errored ? [] : members.data.some((member) => member.votingPowerPercent.loading) ? [] : members.data.sort( (a, b) => (b.votingPowerPercent.loading ? 0 : b.votingPowerPercent.data) - (a.votingPowerPercent.loading ? 0 : a.votingPowerPercent.data) ) // Get members that hold the top voting power. const topMemberUpperIndex = topStakerState < 0 ? -topStakerState : sortedMembers.reduce( (acc, member, index) => ({ total: acc.total + (member.votingPowerPercent.loading ? 0 : member.votingPowerPercent.data), // If the current total is past the top x%, keep the index the same. This // index is the index of the last member in the top x% of voting power. index: acc.total >= topStakerState ? acc.index : index, }), { total: 0, index: 0 } ).index + 1 const topMembers = sortedMembers.slice(0, topMemberUpperIndex) const topMembersVotingPowerPercent = topMembers.reduce( (acc, member) => acc + (member.votingPowerPercent.loading ? 0 : member.votingPowerPercent.data), 0 ) const otherVotingPowerPercent = members.loading || members.errored ? 0 : members.data .slice(topMemberUpperIndex) .reduce( (acc, member) => acc + (member.votingPowerPercent.loading ? 0 : member.votingPowerPercent.data), 0 ) return ( <> {/* header min-height of 3.5rem standardized across all tabs */} {addMemberHref && (

{t('title.newMember')}

{t('info.newMemberExplanation')}

{t('button.addMembers')} {t('button.add')}
)}

{members.loading || members.errored ? t('title.members') : t('title.numMembers', { count: members.data.length })}

{topVoters.show && topMembers.length > 0 && (

{t('title.whoControlsDaoQuestion')}

0 ? [ { label: topStakerStateOption?.label ?? t('title.total'), votingPowerPercent: topMembersVotingPowerPercent, section: 2, color: 'var(--text-tertiary)', }, ] : []), ...(otherVotingPowerPercent > 0 ? [ { label: t('title.otherMembers'), votingPowerPercent: otherVotingPowerPercent, section: 2, color: 'var(--text-interactive-disabled)', }, ] : []), ]} />
)} {members.loading ? ( ) : members.errored ? ( ) : members.data.length ? ( <> {members.data .slice( (membersPage - 1) * MEMBERS_PER_PAGE, membersPage * MEMBERS_PER_PAGE ) .map((props, index) => ( ))} ) : (

{t('error.noMembers')}

)} {!members.loading && !members.errored && members.data.length > 0 && ( <> [ address, balance.loading ? '...' : HugeDecimal.from( balance.data.amount ).toHumanReadableString( balance.data.token?.decimals ?? 0 ), votingPowerPercent.loading ? '...' : votingPowerPercent.data, ] ), ]} filename="members.csv" ref={(ref: any) => (csvLinkRef.current = ref?.link ?? undefined)} /> )} ) }