mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-08-11 15:51:06 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletSocietyBid } from '@pezkuwi/types/lookup';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Table, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import BidType from './BidType.js';
|
||||
|
||||
interface Props {
|
||||
index: number;
|
||||
value: PalletSocietyBid;
|
||||
}
|
||||
|
||||
function BidRow ({ index, value: { kind, value, who } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const [voucher, tip] = useMemo(
|
||||
() => kind.isVouch
|
||||
? kind.asVouch
|
||||
: [null, null],
|
||||
[kind]
|
||||
);
|
||||
|
||||
const [isBidder, isVoucher] = useMemo(
|
||||
(): [boolean, boolean] => {
|
||||
const whoSS58 = who.toString();
|
||||
const vouchSS58 = voucher?.toString();
|
||||
|
||||
return [
|
||||
allAccounts.some((accountId) => accountId === whoSS58),
|
||||
vouchSS58
|
||||
? allAccounts.some((accountId) => accountId === vouchSS58)
|
||||
: false
|
||||
];
|
||||
},
|
||||
[allAccounts, voucher, who]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className='address all'>
|
||||
<AddressSmall value={who} />
|
||||
</td>
|
||||
<td className='start'>
|
||||
<BidType value={kind} />
|
||||
{kind.isVouch
|
||||
? isVoucher && (
|
||||
<TxButton
|
||||
accountId={voucher}
|
||||
icon='times'
|
||||
label={t('Unvouch')}
|
||||
params={[index]}
|
||||
tx={api.tx.society.unvouch}
|
||||
/>
|
||||
)
|
||||
: isBidder && (
|
||||
<TxButton
|
||||
accountId={who}
|
||||
icon='times'
|
||||
label={t('Unbid')}
|
||||
params={[index]}
|
||||
tx={api.tx.society.unbid}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
<Table.Column.Balance value={value} />
|
||||
<Table.Column.Balance value={tip} />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BidRow);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function BidNew ({ onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const [accountId, setAccount] = useState<string | null | undefined>();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header= {t('Bid to join')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('Your candidate/bid account. Once accepted this account will become a member.')}>
|
||||
<InputAddress
|
||||
label={t('bid account')}
|
||||
onChange={setAccount}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount to tie to your bid. The lowest bidder moves forward.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
label={t('bid amount')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!amount}
|
||||
label={t('Bid')}
|
||||
onStart={onClose}
|
||||
params={[amount]}
|
||||
tx={api.tx.society.bid}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BidNew);
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletSocietyBidKind } from '@pezkuwi/types/lookup';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, styled } from '@pezkuwi/react-components';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value?: PalletSocietyBidKind;
|
||||
}
|
||||
|
||||
function BidType ({ className, value }: Props): React.ReactElement<Props> {
|
||||
const vouchId = useMemo(
|
||||
() => value?.isVouch
|
||||
? value.asVouch[0]
|
||||
: null,
|
||||
[value]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<div>{value?.type}</div>
|
||||
{vouchId && <AddressSmall value={vouchId} />}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: no-wrap;
|
||||
|
||||
> div {
|
||||
flex: 0;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(BidType);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletSocietyBid } from '@pezkuwi/types/lookup';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import BidRow from './Bid.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Bids ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const bids = useCall<PalletSocietyBid[]>(api.query.society.bids);
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('bids'), 'start'],
|
||||
[t('bid kind'), 'start'],
|
||||
[t('value')],
|
||||
[t('tip')]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={bids && t('No bids')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{bids?.map((bid, index): React.ReactNode => (
|
||||
<BidRow
|
||||
index={index}
|
||||
key={bid.who.toString()}
|
||||
value={bid}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Bids);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate } from '@pezkuwi/api-derive/types';
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { AccountId, SocietyVote } from '@pezkuwi/types/interfaces';
|
||||
import type { VoteType } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Votes from '../Overview/Votes.js';
|
||||
import BidType from './BidType.js';
|
||||
import CandidateVoting from './CandidateVoting.js';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
value: DeriveSocietyCandidate;
|
||||
}
|
||||
|
||||
function Candidate ({ allMembers, isMember, ownMembers, value: { accountId, kind, value } }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
const keys = useMemo(
|
||||
() => [allMembers.map((memberId): [AccountId, string] => [accountId, memberId])],
|
||||
[accountId, allMembers]
|
||||
);
|
||||
const votes = useCall<VoteType[]>(api.query.society.votes.multi, keys, {
|
||||
transform: (voteOpts: Option<SocietyVote>[]): VoteType[] =>
|
||||
voteOpts
|
||||
.map((voteOpt, index): [string, Option<SocietyVote>] => [allMembers[index], voteOpt])
|
||||
.filter(([, voteOpt]) => voteOpt.isSome)
|
||||
.map(([accountId, voteOpt]): VoteType => [accountId, voteOpt.unwrap()])
|
||||
});
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className='address all'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td className='start'>
|
||||
<BidType value={kind} />
|
||||
</td>
|
||||
<Table.Column.Balance value={value} />
|
||||
<Votes votes={votes} />
|
||||
<td className='button'>
|
||||
<CandidateVoting
|
||||
candidateId={accountId.toString()}
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Candidate);
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, InputAddress, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
candidateId: string;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function CandidateVoting ({ candidateId, isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isVisible, toggleVisible] = useToggle();
|
||||
const [vote, setVote] = useState(true);
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
|
||||
const voteOptsRef = useRef([
|
||||
{ text: t('Aye, I approve'), value: true },
|
||||
{ text: t('Nay, I do not approve'), value: false }
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVisible && (
|
||||
<Modal
|
||||
header={t('Vote for candidate')}
|
||||
onClose={toggleVisible}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
filter={ownMembers}
|
||||
label={t('vote from account')}
|
||||
onChange={setAccountId}
|
||||
/>
|
||||
<Dropdown
|
||||
label={t('vote for candidate')}
|
||||
onChange={setVote}
|
||||
options={voteOptsRef.current}
|
||||
value={vote}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label={t('Vote')}
|
||||
onStart={toggleVisible}
|
||||
params={[candidateId, vote]}
|
||||
tx={api.tx.society.vote}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='check'
|
||||
isDisabled={!isMember}
|
||||
label={t('Vote')}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CandidateVoting);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Candidate from './Candidate.js';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
candidates?: DeriveSocietyCandidate[];
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function Candidates ({ allMembers, candidates, className = '', isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('candidates'), 'start'],
|
||||
[t('bid kind'), 'start'],
|
||||
[t('value')],
|
||||
[undefined, 'expand'],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={candidates && t('No candidates')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{candidates?.map((candidate): React.ReactNode => (
|
||||
<Candidate
|
||||
allMembers={allMembers}
|
||||
isMember={isMember}
|
||||
key={candidate.accountId.toString()}
|
||||
ownMembers={ownMembers}
|
||||
value={candidate}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Candidates);
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function VouchNew ({ allMembers, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const [tip, setTip] = useState<BN | undefined>();
|
||||
const [accountId, setAccount] = useState<string | null | undefined>();
|
||||
const [candidateId, setCandidate] = useState<string | null | undefined>();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header= {t('Vouch for a new candidate')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('Your member account that the vouch is made from.')}>
|
||||
<InputAddress
|
||||
filter={allMembers}
|
||||
label={t('member account')}
|
||||
onChange={setAccount}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The candidate/bid account. Once accepted this account will become a member.')}>
|
||||
<InputAddress
|
||||
label={t('bid account')}
|
||||
onChange={setCandidate}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount to tie to your bid. The lowest bidder moves forward.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
label={t('bid amount')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount you wish to be tipped for your bid.')}>
|
||||
<InputBalance
|
||||
label={t('tip amount')}
|
||||
onChange={setTip}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!amount || !candidateId || !tip}
|
||||
label={t('Vouch')}
|
||||
onStart={onClose}
|
||||
params={[candidateId, amount, tip]}
|
||||
tx={api.tx.society.vouch}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(VouchNew);
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate } from '@pezkuwi/api-derive/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import BidNew from './BidNew.js';
|
||||
import Bids from './Bids.js';
|
||||
import AllCandidates from './Candidates.js';
|
||||
import VouchFor from './VouchFor.js';
|
||||
|
||||
interface Props {
|
||||
allMembers: string[];
|
||||
candidates?: DeriveSocietyCandidate[];
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function Candidates ({ allMembers, candidates, className, isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [isBidOpen, toggleBidOpen] = useToggle();
|
||||
const [isVouchOpen, toggleVouchOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Submit bid')}
|
||||
onClick={toggleBidOpen}
|
||||
/>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!isMember}
|
||||
label={t('Vouch for')}
|
||||
onClick={toggleVouchOpen}
|
||||
/>
|
||||
{isBidOpen && (
|
||||
<BidNew onClose={toggleBidOpen} />
|
||||
)}
|
||||
{isVouchOpen && (
|
||||
<VouchFor
|
||||
allMembers={allMembers}
|
||||
onClose={toggleVouchOpen}
|
||||
/>
|
||||
)}
|
||||
</Button.Group>
|
||||
<AllCandidates
|
||||
allMembers={allMembers}
|
||||
candidates={candidates}
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
<Bids />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Candidates);
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety, DeriveSocietyMember } from '@pezkuwi/api-derive/types';
|
||||
import type { SocietyVote } from '@pezkuwi/types/interfaces';
|
||||
import type { VoteType } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { AddressSmall, Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import DefenderVoting from './DefenderVoting.js';
|
||||
import Votes from './Votes.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
info?: DeriveSociety;
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
const OPT_VOTES = {
|
||||
transform: (members: DeriveSocietyMember[]): VoteType[] =>
|
||||
members
|
||||
.filter(({ vote }): boolean => !!vote)
|
||||
.map(({ accountId, vote }): VoteType => [accountId.toString(), vote as unknown as SocietyVote])
|
||||
};
|
||||
|
||||
function Defender ({ className = '', info, isMember, ownMembers }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const votes = useCall<VoteType[]>(api.derive.society.members, undefined, OPT_VOTES);
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('defender'), 'start'],
|
||||
[undefined, 'expand'],
|
||||
[]
|
||||
]);
|
||||
|
||||
if (!info || !info.hasDefender || !info.defender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
header={headerRef.current}
|
||||
>
|
||||
<tr>
|
||||
<td className='address all'>
|
||||
<AddressSmall value={info.defender} />
|
||||
</td>
|
||||
<Votes votes={votes} />
|
||||
<td className='button'>
|
||||
<DefenderVoting
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Defender);
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, InputAddress, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
function DefenderVoting ({ isMember, ownMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isVisible, toggleVisible] = useToggle();
|
||||
const [vote, setVote] = useState(true);
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
|
||||
const voteOptsRef = useRef([
|
||||
{ text: t('Aye, I approve'), value: true },
|
||||
{ text: t('Nay, I do not approve'), value: false }
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVisible && (
|
||||
<Modal
|
||||
header={t('Vote for defender')}
|
||||
onClose={toggleVisible}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
filter={ownMembers}
|
||||
label={t('vote from account')}
|
||||
onChange={setAccountId}
|
||||
/>
|
||||
<Dropdown
|
||||
label={t('vote for defender')}
|
||||
onChange={setVote}
|
||||
options={voteOptsRef.current}
|
||||
value={vote}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label={t('Vote')}
|
||||
onStart={toggleVisible}
|
||||
params={[vote]}
|
||||
tx={api.tx.society.defenderVote}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='check'
|
||||
isDisabled={!isMember}
|
||||
label={t('Vote')}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DefenderVoting);
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { DICLE_GENESIS } from '@pezkuwi/apps-config';
|
||||
import { Button, Modal } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import drawCanary, { PADD, SIZE } from '../draw/canary.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
accountId: AccountId;
|
||||
}
|
||||
|
||||
const CANVAS_STYLE = {
|
||||
display: 'block',
|
||||
margin: '0 auto'
|
||||
};
|
||||
|
||||
const HEIGHT = (SIZE * 2) + (PADD * 1);
|
||||
const WIDTH = (SIZE * 3) + (PADD * 2);
|
||||
|
||||
function DesignDicle ({ accountId }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [onDicle] = useState(() => api.genesisHash.eq(DICLE_GENESIS));
|
||||
const [isShowing, toggleDesign] = useToggle();
|
||||
|
||||
useEffect((): void => {
|
||||
if (canvasRef.current) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
|
||||
if (ctx) {
|
||||
drawCanary(ctx, accountId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!onDicle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='pen-nib'
|
||||
onClick={toggleDesign}
|
||||
/>
|
||||
{isShowing && (
|
||||
<Modal
|
||||
header={t('design samples')}
|
||||
onClose={toggleDesign}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<canvas
|
||||
height={HEIGHT}
|
||||
ref={canvasRef}
|
||||
style={CANVAS_STYLE}
|
||||
width={WIDTH}
|
||||
/>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DesignDicle);
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Balance, BlockNumber } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { MapMember } from '../types.js';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Columar, Expander, styled, Tag, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
|
||||
import { BlockToTime, FormatBalance } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import DesignDicle from './DesignDicle.js';
|
||||
|
||||
interface Props {
|
||||
bestNumber?: BN;
|
||||
className?: string;
|
||||
value: MapMember;
|
||||
}
|
||||
|
||||
function renderJSXPayouts (bestNumber: BN, payouts: [BlockNumber, Balance][]): React.ReactElement<unknown>[] {
|
||||
return payouts.map(([bn, value], index) => (
|
||||
<div
|
||||
className='payout'
|
||||
key={index}
|
||||
>
|
||||
<Columar>
|
||||
<Columar.Column>
|
||||
<FormatBalance value={value} />
|
||||
</Columar.Column>
|
||||
<Columar.Column>
|
||||
<div>#{formatNumber(bn)}</div>
|
||||
{bn.gt(bestNumber) && (
|
||||
<BlockToTime
|
||||
key={index}
|
||||
value={bn.sub(bestNumber)}
|
||||
/>
|
||||
)}
|
||||
</Columar.Column>
|
||||
</Columar>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
function Member ({ bestNumber, className = '', value: { accountId, isCandidateVoter, isDefenderVoter, isFounder, isHead, isSkeptic, isSuspended, isWarned, key, payouts, strikes } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const renderPayouts = useCallback(
|
||||
() => bestNumber && payouts && renderJSXPayouts(bestNumber, payouts),
|
||||
[bestNumber, payouts]
|
||||
);
|
||||
|
||||
const isOwner = useMemo(
|
||||
() => allAccounts.some((a) => a === key),
|
||||
[allAccounts, key]
|
||||
);
|
||||
|
||||
const availablePayout = useMemo(
|
||||
() => bestNumber && payouts.find(([b]) => bestNumber.gt(b)),
|
||||
[bestNumber, payouts]
|
||||
);
|
||||
|
||||
const votedOn = useMemo(
|
||||
() => [isCandidateVoter && t('Candidate'), isDefenderVoter && t('Defender')]
|
||||
.filter((s): s is string => !!s)
|
||||
.join(', '),
|
||||
[isCandidateVoter, isDefenderVoter, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTr className={className}>
|
||||
<td className='address relative all'>
|
||||
<AddressSmall value={accountId} />
|
||||
<div className='absolute'>
|
||||
{(isCandidateVoter || isDefenderVoter) && (
|
||||
<Tag
|
||||
color='blue'
|
||||
label={t('voted')}
|
||||
/>
|
||||
)}
|
||||
{isWarned && (
|
||||
<Tag
|
||||
color='orange'
|
||||
label={t('strikes')}
|
||||
/>
|
||||
)}
|
||||
{isHead && (
|
||||
<Tag
|
||||
color='green'
|
||||
label={t('society head')}
|
||||
/>
|
||||
)}
|
||||
{isFounder && (
|
||||
<Tag
|
||||
color='green'
|
||||
label={t('founder')}
|
||||
/>
|
||||
)}
|
||||
{isSkeptic && (
|
||||
<Tag
|
||||
color='yellow'
|
||||
label={t('skeptic')}
|
||||
/>
|
||||
)}
|
||||
{isSuspended && (
|
||||
<Tag
|
||||
color='red'
|
||||
label={t('suspended')}
|
||||
/>
|
||||
)}
|
||||
{availablePayout && (
|
||||
<Tag
|
||||
color='grey'
|
||||
label={t('payout')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className='number together'>
|
||||
{!!payouts?.length && (
|
||||
<Expander
|
||||
className='payoutExpander'
|
||||
renderChildren={renderPayouts}
|
||||
summary={t('Payouts ({{count}})', { replace: { count: formatNumber(payouts.length) } })}
|
||||
/>
|
||||
)}
|
||||
{isOwner && availablePayout && (
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='ellipsis-h'
|
||||
label='Payout'
|
||||
params={[]}
|
||||
tx={api.tx.society.payout}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='together'>{votedOn}</td>
|
||||
<td className='number'>{formatNumber(strikes)}</td>
|
||||
<td className='button start'>
|
||||
<DesignDicle accountId={accountId} />
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
.payoutExpander {
|
||||
.payout+.payout {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.ui--Columar {
|
||||
flex-wrap: unset;
|
||||
|
||||
.ui--Column {
|
||||
min-width: 15ch;
|
||||
|
||||
&:first-child {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
min-width: 15ch;
|
||||
max-width: 15ch;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Member);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { MapMember } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useBestNumber } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Member from './Member.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
mapMembers?: MapMember[];
|
||||
}
|
||||
|
||||
function Members ({ className = '', mapMembers }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('members'), 'start', 2],
|
||||
[t('voted on'), 'start'],
|
||||
[t('strikes')],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={mapMembers && t('No active members')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{mapMembers?.map((value): React.ReactNode => (
|
||||
<Member
|
||||
bestNumber={bestNumber}
|
||||
key={value.key}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Members);
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety } from '@pezkuwi/api-derive/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { CardSummary, styled, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useBestNumber, useCall } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
info?: DeriveSociety;
|
||||
payoutTotal?: BN;
|
||||
}
|
||||
|
||||
function Summary ({ className = '', info, payoutTotal }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const members = useCall<unknown[]>(api.derive.society.members);
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
const pot = useMemo(
|
||||
() => info && info.pot.gtn(0)
|
||||
? info.pot
|
||||
: null,
|
||||
[info]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSummaryBox className={className}>
|
||||
<section className='media--1100'>
|
||||
{info && members && (
|
||||
<CardSummary label={t('members')}>
|
||||
{members.length} / {info.maxMembers?.toString()}
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
{bestNumber && (
|
||||
<>
|
||||
{api.consts.society.rotationPeriod && (
|
||||
<section>
|
||||
<CardSummary
|
||||
label={t('rotation')}
|
||||
progress={{
|
||||
total: api.consts.society.rotationPeriod as unknown as BN,
|
||||
value: bestNumber.mod(api.consts.society.rotationPeriod as unknown as BN),
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section className='media--1200'>
|
||||
<CardSummary
|
||||
label={t('challenge')}
|
||||
progress={{
|
||||
total: api.consts.society.challengePeriod,
|
||||
value: bestNumber.mod(api.consts.society.challengePeriod),
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<section>
|
||||
{payoutTotal && (
|
||||
<CardSummary label={t('payouts')}>
|
||||
<FormatBalance
|
||||
value={payoutTotal}
|
||||
withSi
|
||||
/>
|
||||
</CardSummary>
|
||||
)}
|
||||
{pot && (
|
||||
<CardSummary label={t('pot')}>
|
||||
<FormatBalance
|
||||
value={pot}
|
||||
withSi
|
||||
/>
|
||||
</CardSummary>
|
||||
)}
|
||||
</section>
|
||||
</StyledSummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledSummaryBox = styled(SummaryBox)`
|
||||
.society--header--account {
|
||||
white-space: nowrap;
|
||||
|
||||
.ui--AccountName {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ui--IdentityIcon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { VoteSplit, VoteType } from '../types.js';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import VotesExpander from './VotesExpander.js';
|
||||
|
||||
interface Props {
|
||||
votes?: VoteType[];
|
||||
}
|
||||
|
||||
function Votes ({ votes }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [{ allAye, allNay, allSkeptic }, setVoteSplit] = useState<VoteSplit>({ allAye: [], allNay: [], allSkeptic: [] });
|
||||
|
||||
useEffect((): void => {
|
||||
votes && setVoteSplit({
|
||||
allAye: votes.filter(([, vote]) => vote.isApprove),
|
||||
allNay: votes.filter(([, vote]) => vote.isReject),
|
||||
allSkeptic: votes.filter(([, vote]) => vote.isSkeptic)
|
||||
});
|
||||
}, [votes]);
|
||||
|
||||
return (
|
||||
<td className='expand'>
|
||||
<VotesExpander
|
||||
label={t('Skeptics')}
|
||||
votes={allSkeptic}
|
||||
/>
|
||||
<VotesExpander
|
||||
label={t('Approvals')}
|
||||
votes={allAye}
|
||||
/>
|
||||
<VotesExpander
|
||||
label={t('Rejections')}
|
||||
votes={allNay}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Votes);
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { VoteType } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressMini, Expander } from '@pezkuwi/react-components';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
votes: VoteType[];
|
||||
}
|
||||
|
||||
function VotesExpander ({ label, votes }: Props): React.ReactElement<Props> | null {
|
||||
if (votes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Expander summary={`${label} (${votes.length})`}>
|
||||
{votes.map(([who]): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={who.toString()}
|
||||
value={who}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(VotesExpander);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSociety } from '@pezkuwi/api-derive/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { MapMember } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
|
||||
import Defender from './Defender.js';
|
||||
import Members from './Members.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
info?: DeriveSociety;
|
||||
isMember: boolean;
|
||||
mapMembers?: MapMember[];
|
||||
ownMembers: string[];
|
||||
payoutTotal?: BN;
|
||||
}
|
||||
|
||||
function Overview ({ className, info, isMember, mapMembers, ownMembers, payoutTotal }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<Summary
|
||||
info={info}
|
||||
payoutTotal={payoutTotal}
|
||||
/>
|
||||
<Defender
|
||||
info={info}
|
||||
isMember={isMember}
|
||||
ownMembers={ownMembers}
|
||||
/>
|
||||
<Members mapMembers={mapMembers} />
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.overviewSection {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AccountId, BalanceOf } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletSocietyBidKind } from '@pezkuwi/types/lookup';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall, Table } from '@pezkuwi/react-components';
|
||||
|
||||
import BidType from '../Candidates/BidType.js';
|
||||
|
||||
interface Props {
|
||||
balance?: BalanceOf;
|
||||
bid?: PalletSocietyBidKind;
|
||||
value: AccountId;
|
||||
}
|
||||
|
||||
function Suspension ({ balance, bid, value }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<tr>
|
||||
<td className='address all'>
|
||||
<AddressSmall value={value} />
|
||||
</td>
|
||||
<td className='start'>
|
||||
<BidType value={bid} />
|
||||
</td>
|
||||
<Table.Column.Balance value={balance} />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Suspension);
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, StorageKey } from '@pezkuwi/types';
|
||||
import type { AccountId, BalanceOf } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletSocietyBidKind } from '@pezkuwi/types/lookup';
|
||||
import type { ITuple } from '@pezkuwi/types/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Suspension from './Suspension.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface CandidateSuspend {
|
||||
accountId: AccountId;
|
||||
balance: BalanceOf;
|
||||
bid: PalletSocietyBidKind;
|
||||
}
|
||||
|
||||
const OPT_CAN = {
|
||||
transform: (entries: [StorageKey<[AccountId]>, Option<ITuple<[BalanceOf, PalletSocietyBidKind]>>][]): CandidateSuspend[] =>
|
||||
entries
|
||||
.filter(([{ args: [accountId] }, opt]) => opt.isSome && accountId)
|
||||
.map(([{ args: [accountId] }, opt]) => {
|
||||
const [balance, bid] = opt.unwrap();
|
||||
|
||||
return { accountId, balance, bid };
|
||||
})
|
||||
.sort((a, b) => a.balance.cmp(b.balance))
|
||||
};
|
||||
|
||||
const OPT_ACC = {
|
||||
transform: (keys: StorageKey<[AccountId]>[]): AccountId[] =>
|
||||
keys
|
||||
.map(({ args: [accountId] }) => accountId)
|
||||
.filter((a) => !!a)
|
||||
};
|
||||
|
||||
function Suspended ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const candidates = useCall<CandidateSuspend[]>(api.query.society.suspendedCandidates?.entries, undefined, OPT_CAN) ?? [];
|
||||
const members = useCall<AccountId[]>(api.query.society.suspendedMembers.keys, undefined, OPT_ACC);
|
||||
|
||||
const headerRef = useRef({
|
||||
candidates: [
|
||||
[t('candidates'), 'start'],
|
||||
[t('bid kind'), 'start'],
|
||||
[t('value')]
|
||||
] as [React.ReactNode?, string?, number?][],
|
||||
members: [
|
||||
[t('members'), 'start', 3]
|
||||
] as [React.ReactNode?, string?, number?][]
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Table
|
||||
className={className}
|
||||
empty={members && t('No suspended members')}
|
||||
header={headerRef.current.members}
|
||||
>
|
||||
{members?.map((accountId): React.ReactNode => (
|
||||
<Suspension
|
||||
key={accountId.toString()}
|
||||
value={accountId}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
<Table
|
||||
className={className}
|
||||
empty={candidates && t('No suspended candidates')}
|
||||
header={headerRef.current.candidates}
|
||||
>
|
||||
{candidates?.map(({ accountId, balance, bid }): React.ReactNode => (
|
||||
<Suspension
|
||||
balance={balance}
|
||||
bid={bid}
|
||||
key={accountId.toString()}
|
||||
value={accountId}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Suspended);
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Adapted (with permission) from https://www.w3schools.com/code/tryit.asp?filename=GGIGKE2GG7N1
|
||||
|
||||
import type { AccountId } from '@pezkuwi/types/interfaces';
|
||||
|
||||
// const DEFAULT_FN = (ctx: CanvasRenderingContext2D, path: Path2D) => ctx.fill(path);
|
||||
export const PADD = 25;
|
||||
export const SIZE = 300; // 250
|
||||
|
||||
function canary (ctx: CanvasRenderingContext2D, w: number, h: number, s: number, f: (ctx: CanvasRenderingContext2D, path: Path2D) => void): void {
|
||||
const path = new Path2D('M373.1,126.9c-5.2-4.1-11.4-9.7-22.7-11.1c-10.6-1.4-21.4,5.7-28.7,10.4c-7.3,4.7-21.1,18.5-26.8,22.7 c-5.7,4.2-20.3,8.1-43.8,22.2s-115.7,73.3-115.7,73.3l24,0.3L52.4,299.8h10.7l-15.4,11.7c0,0,13.6,3.6,25-3.6l0,3.3 c0,0,127.4-50.2,152-37.2l-15,4.4c1.3,0,25.5,1.6,25.5,1.6s0.8,15.1,15.4,24.8c14.6,9.6,14.9,14.9,14.9,14.9s-7.6,3.1-7.6,7 c0,0,11.2-3.4,21.6-3.1c10.4,0.3,19.5,3.1,19.5,3.1s-0.8-4.2-10.9-7c-10.2-2.9-20.1-13.8-25-19.8c-4.9-6-8.3-16.7-4.1-27.4 c3.5-9.1,15.7-14.1,40.9-27.1c29.7-15.4,36.5-26.8,40.7-35.7c4.2-8.9,10.4-26.6,13.9-34.9c4.4-10.7,9.8-16.4,14.3-19.8 c4.4-3.4,24.5-10.9,24.5-10.9S378,130.8,373.1,126.9z');
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(w / 2, h / 2);
|
||||
ctx.scale(s / 440, s / 440);
|
||||
ctx.translate(-220, -220);
|
||||
f(ctx, path);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function addressToBits (publicKey: Uint8Array): boolean[] {
|
||||
return publicKey.reduce((bits: boolean[], byte): boolean[] => {
|
||||
for (let j = 0; j < 8; ++j) {
|
||||
bits.push((byte & (1 << (7 - j))) !== 0);
|
||||
}
|
||||
|
||||
return bits;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function ring (ctx: CanvasRenderingContext2D, r: number, bits: boolean[], f: (ctx: CanvasRenderingContext2D, on: boolean) => void): void {
|
||||
ctx.save();
|
||||
ctx.translate(0.5, 0.5);
|
||||
|
||||
for (let i = 0; i < bits.length; i++) {
|
||||
ctx.save();
|
||||
ctx.rotate(Math.PI * 2 / bits.length * i);
|
||||
ctx.translate(0, -r);
|
||||
f(ctx, bits[i]);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function splitRows (bits: boolean[], rows: number[]) {
|
||||
let i = 0;
|
||||
|
||||
// eslint-disable-next-line no-return-assign
|
||||
return rows.map((r) => bits.slice(i, i += r));
|
||||
}
|
||||
|
||||
function tattoo (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
const rows = splitRows(bits, [71, 61, 51, 41, 32]);
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
ring(ctx, 0.5 - (31 / 500) * (i + 0.5), rows[i], (ctx, on) => {
|
||||
if (on) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 8 / 500, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ctx.lineWidth = 10;
|
||||
canary(ctx, 1, 1, 200 / 500, (ctx, path) => ctx.stroke(path));
|
||||
}
|
||||
|
||||
function tattooSpiro (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
const cycles = 8;
|
||||
const limit = 0.75;
|
||||
const dot = 8 / 500;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(0.5 - dot, 0.5 + dot);
|
||||
ctx.fillStyle = 'black';
|
||||
|
||||
let radius = 0.5 - dot;
|
||||
|
||||
for (let i = 0, count = bits.length; i < count; i++) {
|
||||
radius -= 0.5 / count * limit / (radius * 4);
|
||||
ctx.rotate(Math.PI * 2 / count * cycles / (radius * 4));
|
||||
ctx.save();
|
||||
ctx.translate(0, -radius);
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, (bits[i] ? dot : dot / 2), 0, 2 * Math.PI);
|
||||
ctx.fillStyle = bits[i] ? 'black' : '#e6007a';
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
ctx.lineWidth = 10;
|
||||
canary(ctx, 1 - dot, 1 + dot, 220 / 500, (ctx, path) => ctx.stroke(path));
|
||||
}
|
||||
|
||||
function tattooPink (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
const rows = splitRows(bits, [71, 61, 51, 41, 32]);
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
ring(ctx, 0.5 - (31 / 500) * (i + 0.5), rows[i], (ctx, on) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, (on ? 8 : 4) / 500, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = on ? 'black' : '#e6007a';
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
canary(ctx, 1, 1, 220 / 500, (ctx, path) => ctx.fill(path));
|
||||
}
|
||||
|
||||
function tattoo2 (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
const rows = splitRows(bits, [64, 64, 64, 64]);
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
ring(ctx, 0.5 - (36 / 500) * (i + 0.5), rows[i], (ctx, on) => {
|
||||
if (on) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -18 / 500);
|
||||
ctx.lineTo(0, 18 / 500);
|
||||
ctx.lineWidth = 0.01;
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canary(ctx, 1, 1, 220 / 500, (ctx, path) => ctx.fill(path));
|
||||
}
|
||||
|
||||
function tattoo2b (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
const rows = splitRows(bits, [128, 128]);
|
||||
|
||||
for (let i = 0; i < rows.length; ++i) {
|
||||
ring(ctx, 0.5 - (36 / 500) * (i + 0.5), rows[i], (ctx, on) => {
|
||||
if (on) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -18 / 500);
|
||||
ctx.lineTo(0, 18 / 500);
|
||||
ctx.lineWidth = 0.01;
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ctx.lineWidth = 6;
|
||||
canary(ctx, 1, 1, 350 / 500, (ctx, path) => ctx.stroke(path));
|
||||
}
|
||||
|
||||
function tattoo3 (ctx: CanvasRenderingContext2D, bits: boolean[]): void {
|
||||
ctx.lineWidth = 0.01;
|
||||
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
for (let j = 0; j < 32; ++j) {
|
||||
if (bits[i * 32 + j]) {
|
||||
ctx.save();
|
||||
ctx.translate((j + 0.5) / 32, i / 8);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(0, 1 / 8);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
canary(ctx, 1, 1, 1, (ctx, path) => ctx.fill(path));
|
||||
ctx.lineWidth = 6;
|
||||
canary(ctx, 1, 1, 1, (ctx, path) => ctx.stroke(path));
|
||||
}
|
||||
|
||||
export default function draw (ctx: CanvasRenderingContext2D, accountId: AccountId): void {
|
||||
console.log(`Generating ink for ${accountId.toString()} as ${accountId.toHex()}`);
|
||||
|
||||
const bits = addressToBits(accountId.toU8a());
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(0, 0);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattoo(ctx, bits);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(SIZE + PADD, 0);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattooPink(ctx, bits);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(0, SIZE + PADD);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattoo2(ctx, bits);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(SIZE + PADD, SIZE + PADD);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattoo3(ctx, bits);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate((SIZE + PADD) * 2, 0);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattooSpiro(ctx, bits);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.translate((SIZE + PADD) * 2, SIZE + PADD);
|
||||
ctx.scale(SIZE, SIZE);
|
||||
tattoo2b(ctx, bits);
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2017-2025 @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);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-society');
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyCandidate } from '@pezkuwi/api-derive/types';
|
||||
import type { AccountId, Balance, BlockNumber, SocietyVote, StrikeCount } from '@pezkuwi/types/interfaces';
|
||||
|
||||
export interface MapMember {
|
||||
accountId: AccountId;
|
||||
isCandidateVoter: boolean;
|
||||
isDefenderVoter: boolean
|
||||
isFounder: boolean;
|
||||
isHead: boolean;
|
||||
isSkeptic: boolean;
|
||||
isSuspended: boolean;
|
||||
isWarned: boolean;
|
||||
key: string;
|
||||
strikes: StrikeCount;
|
||||
payouts: [BlockNumber, Balance][];
|
||||
}
|
||||
|
||||
export interface OwnMembers {
|
||||
allMembers: string[];
|
||||
isMember: boolean;
|
||||
ownMembers: string[];
|
||||
}
|
||||
|
||||
export type VoteType = [string, SocietyVote];
|
||||
|
||||
export interface VoteSplit {
|
||||
allAye: VoteType[];
|
||||
allNay: VoteType[];
|
||||
allSkeptic: VoteType[];
|
||||
}
|
||||
|
||||
export interface Voters {
|
||||
candidates?: DeriveSocietyCandidate[];
|
||||
skeptics?: string[];
|
||||
voters?: string[];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Bid } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
function useCounterImpl (): number {
|
||||
const { api } = useApi();
|
||||
const bids = useCall<Bid[]>(api.query.society?.candidates);
|
||||
|
||||
return bids?.length || 0;
|
||||
}
|
||||
|
||||
export default createNamedHook('useCounter', useCounterImpl);
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveSocietyMember } from '@pezkuwi/api-derive/types';
|
||||
import type { OwnMembers } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
const EMPTY_MEMBERS: OwnMembers = { allMembers: [], isMember: false, ownMembers: [] };
|
||||
|
||||
function transform (allAccounts: string[], members: DeriveSocietyMember[]): OwnMembers {
|
||||
const allMembers = members
|
||||
.filter(({ isSuspended }) => !isSuspended)
|
||||
.map(({ accountId }) => accountId.toString());
|
||||
const ownMembers = allMembers.filter((a) => allAccounts.includes(a));
|
||||
|
||||
return { allMembers, isMember: ownMembers.length !== 0, ownMembers };
|
||||
}
|
||||
|
||||
function useMembersImpl (): OwnMembers {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const [state, setState] = useState<OwnMembers>(EMPTY_MEMBERS);
|
||||
const members = useCall<DeriveSocietyMember[]>(api.derive.society.members);
|
||||
|
||||
useEffect((): void => {
|
||||
allAccounts && members && setState(
|
||||
transform(allAccounts, members)
|
||||
);
|
||||
}, [allAccounts, members]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useMembers', useMembersImpl);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-society authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { DeriveSocietyCandidate } from '@pezkuwi/api-derive/types';
|
||||
import type { Voters } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall, useEventTrigger } from '@pezkuwi/react-hooks';
|
||||
|
||||
const EMPTY_VOTERS: Voters = {};
|
||||
|
||||
async function getVoters (api: ApiPromise, candidates: DeriveSocietyCandidate[]): Promise<Voters> {
|
||||
const skeptics: string[] = [];
|
||||
const voters: string[] = [];
|
||||
|
||||
const entries = candidates.length
|
||||
? await Promise.all(candidates.map(({ accountId }) =>
|
||||
api.query.society.votes.entries(accountId)
|
||||
))
|
||||
: [];
|
||||
|
||||
entries.forEach((list): void => {
|
||||
list.forEach(([{ args: [, accountId] }, opt]) => {
|
||||
if (opt.isSome) {
|
||||
const key = accountId.toString();
|
||||
const vote = opt.unwrap();
|
||||
|
||||
if ((vote as unknown as { isSkeptic: boolean }).isSkeptic) {
|
||||
!skeptics.includes(key) && skeptics.push(key);
|
||||
} else {
|
||||
!voters.includes(key) && voters.push(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { candidates, skeptics, voters };
|
||||
}
|
||||
|
||||
function useVotersImpl (): Voters {
|
||||
const { api } = useApi();
|
||||
const voteTrigger = useEventTrigger([api.events.society.Vote]);
|
||||
const candidates = useCall<DeriveSocietyCandidate[]>(api.derive.society.candidates);
|
||||
const [state, setState] = useState<Voters>(EMPTY_VOTERS);
|
||||
|
||||
useEffect((): void => {
|
||||
voteTrigger && candidates &&
|
||||
getVoters(api, candidates).then(setState).catch(console.error);
|
||||
}, [api, candidates, voteTrigger]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useVoters', useVotersImpl);
|
||||
Reference in New Issue
Block a user