Files
pezkuwi-apps/packages/page-society/src/index.tsx
T

159 lines
4.8 KiB
TypeScript

// Copyright 2017-2026 @pezkuwi/app-society authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveSociety, DeriveSocietyMember } from '@pezkuwi/api-derive/types';
import type { MapMember } from './types.js';
import React, { useMemo } from 'react';
import { Route, Routes } from 'react-router';
import { Tabs } from '@pezkuwi/react-components';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import { BN, BN_THREE, BN_TWO } from '@pezkuwi/util';
import Candidates from './Candidates/index.js';
import Overview from './Overview/index.js';
import Suspended from './Suspended/index.js';
import { useTranslation } from './translate.js';
import useCounter from './useCounter.js';
import useMembers from './useMembers.js';
import useVoters from './useVoters.js';
interface Props {
basePath: string;
className?: string;
}
export { useCounter };
// head -> founder -> skeptics -> votes -> suspended -> strikes -> strikes -> payouts
function sortMembers (a: MapMember, b: MapMember): number {
const isVoterA = a.isCandidateVoter || a.isDefenderVoter;
return a.isHead !== b.isHead
? (a.isHead ? -1 : 1)
: a.isFounder !== b.isFounder
? (a.isFounder ? -1 : 1)
: a.isSkeptic !== b.isSkeptic
? (a.isSkeptic ? -1 : 1)
: isVoterA !== (b.isCandidateVoter || b.isDefenderVoter)
? (isVoterA ? -1 : 1)
// : a.isDefenderVoter !== b.isDefenderVoter
// ? (a.isDefenderVoter ? -1 : 1)
// : a.isCandidateVoter !== b.isCandidateVoter
// ? (a.isCandidateVoter ? -1 : 1)
: a.isSuspended !== b.isSuspended
? (a.isSuspended ? -1 : 1)
: a.isWarned !== b.isWarned
? (a.isWarned ? -1 : 1)
: (b.strikes.cmp(a.strikes) || (b.payouts.length - a.payouts.length));
}
function getMapMembers (members: DeriveSocietyMember[], skeptics: string[], voters: string[], { defender, founder, hasDefender, head }: DeriveSociety, warnStrikes: BN): [MapMember[], BN] {
const mapMembers = members
.filter((member) => !hasDefender || !member.accountId.eq(defender))
.map(({ accountId, isDefenderVoter, isSuspended, payouts, strikes }): MapMember => {
const key = accountId.toString();
return {
accountId,
isCandidateVoter: voters.includes(key),
isDefenderVoter,
isFounder: !!founder?.eq(accountId),
isHead: !!head?.eq(accountId),
isSkeptic: skeptics.includes(key),
isSuspended,
isWarned: !isSuspended && strikes.gt(warnStrikes),
key,
payouts,
strikes
};
})
.sort(sortMembers);
return [
mapMembers,
mapMembers.reduce((total, { payouts }) =>
payouts.reduce((total, [, balance]) => total.iadd(balance), total), new BN(0)
)
];
}
function SocietyApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const candidateCount = useCounter();
const { allMembers, isMember, ownMembers } = useMembers();
const info = useCall<DeriveSociety>(api.derive.society.info);
const members = useCall<DeriveSocietyMember[]>(api.derive.society.members);
const { candidates, skeptics, voters } = useVoters();
const [mapMembers, payoutTotal] = useMemo(
() => members && info && skeptics && voters
? getMapMembers(members, skeptics, voters, info, (api.consts.society.graceStrikes || api.consts.society.maxStrikes).mul(BN_TWO).div(BN_THREE))
: [undefined, undefined],
[api, info, members, skeptics, voters]
);
const items = useMemo(() => [
{
isRoot: true,
name: 'overview',
text: t('Overview')
},
{
count: candidateCount,
name: 'candidates',
text: t('Candidates')
},
{
name: 'suspended',
text: t('Suspended')
}
], [candidateCount, t]);
return (
<main className={className}>
<Tabs
basePath={basePath}
items={items}
/>
<Routes>
<Route path={basePath}>
<Route
element={
<Candidates
allMembers={allMembers}
candidates={candidates}
isMember={isMember}
ownMembers={ownMembers}
/>
}
path='candidates'
/>
<Route
element={
<Suspended />
}
path='suspended'
/>
<Route
element={
<Overview
info={info}
isMember={isMember}
mapMembers={mapMembers}
ownMembers={ownMembers}
payoutTotal={payoutTotal}
/>
}
index
/>
</Route>
</Routes>
</main>
);
}
export default React.memo(SocietyApp);