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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
@@ -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}&nbsp;/&nbsp;{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);
+216
View File
@@ -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();
}
+158
View File
@@ -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);
+8
View File
@@ -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');
}
+39
View File
@@ -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[];
}
+15
View File
@@ -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);
+37
View File
@@ -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);
+56
View File
@@ -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);