mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-28 17:17:57 +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,88 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useCollectiveMembers } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Overview from './Overview/index.js';
|
||||
import Proposals from './Proposals/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
const HIDDEN_EMPTY: string[] = [];
|
||||
const HIDDEN_PROPOSALS: string[] = ['proposals'];
|
||||
|
||||
function TechCommApp ({ basePath, className, type }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { isMember, members, prime } = useCollectiveMembers(type);
|
||||
const hasProposals = useCall<boolean>(api.derive[type].hasProposals);
|
||||
const proposalHashes = useCall<Hash[]>(api.derive[type].proposalHashes);
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'proposals',
|
||||
text: t('Proposals ({{count}})', { replace: { count: proposalHashes?.length || 0 } })
|
||||
}
|
||||
], [proposalHashes, t]);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
hidden={
|
||||
hasProposals
|
||||
? HIDDEN_EMPTY
|
||||
: HIDDEN_PROPOSALS
|
||||
}
|
||||
items={items}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path={basePath}>
|
||||
<Route
|
||||
element={
|
||||
<Proposals
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
prime={prime}
|
||||
proposalHashes={proposalHashes}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
path='proposals'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Overview
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
prime={prime}
|
||||
proposalHashes={proposalHashes}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
index
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TechCommApp);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { AddressSmall, Table, Tag } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
members?: string[];
|
||||
prime?: string | null;
|
||||
}
|
||||
|
||||
function Members ({ className = '', members, prime }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('members'), 'start', 3]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={members && t('No members found')}
|
||||
header={headerRef.current}
|
||||
isSplit
|
||||
>
|
||||
{members?.map((accountId): React.ReactNode => (
|
||||
<tr key={accountId}>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td>
|
||||
{prime === accountId && (
|
||||
<Tag
|
||||
color='green'
|
||||
hover={t('Committee prime member, default voting')}
|
||||
label={t('prime member')}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='all'> </td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Members);
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { u32 } from '@pezkuwi/types';
|
||||
import type { ComponentProps as Props } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
function Summary ({ className = '', members, proposalHashes, type }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const proposalCount = useCall<u32>(api.derive[type].proposalCount);
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('members')}>
|
||||
{formatNumber(members.length)}
|
||||
</CardSummary>
|
||||
<section>
|
||||
<CardSummary label={t('proposals')}>
|
||||
{proposalHashes
|
||||
? formatNumber(proposalHashes?.length)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('total')}>
|
||||
{proposalCount
|
||||
? formatNumber(proposalCount)
|
||||
: <span className='--tmp'>99</span>}
|
||||
</CardSummary>
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ComponentProps as Props } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Members from './Members.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
function Overview ({ className = '', isMember, members, prime, proposalHashes, type }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
proposalHashes={proposalHashes}
|
||||
type={type}
|
||||
/>
|
||||
<Members
|
||||
members={members}
|
||||
prime={prime}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
import type { Hash, Proposal, ProposalIndex } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCollectiveInstance, useToggle, useWeight } from '@pezkuwi/react-hooks';
|
||||
import { ProposedAction } from '@pezkuwi/react-params';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
hasFailed: boolean;
|
||||
hash: Hash;
|
||||
idNumber: ProposalIndex;
|
||||
proposal: Proposal | null;
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Close ({ hasFailed, hash, idNumber, proposal, type }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const { encodedCallLength, weight } = useWeight(proposal);
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
|
||||
if (!modLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t('Close proposal')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The proposal that will be affected. Once closed for the current voting round, it would need to be re-submitted for a subsequent voting round.')}>
|
||||
<ProposedAction
|
||||
idNumber={idNumber}
|
||||
proposal={proposal}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The committee account that will apply the close for the current round.')}>
|
||||
<InputAddress
|
||||
label={t('close from account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
isDisabled={!hasFailed && !encodedCallLength}
|
||||
onStart={toggleOpen}
|
||||
params={
|
||||
api.tx[modLocation].close.meta.args.length === 4
|
||||
? hasFailed
|
||||
? [hash, idNumber, 0, 0]
|
||||
: [hash, idNumber, weight, encodedCallLength]
|
||||
: [hash, idNumber]
|
||||
}
|
||||
tx={api.tx[modLocation].closeOperational || api.tx[modLocation].close}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='times'
|
||||
label={t('Close')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Close);
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types';
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ProposalCell from '@pezkuwi/app-democracy/Overview/ProposalCell';
|
||||
import { AddressMini, Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useCollectiveInstance, useVotingStatus } from '@pezkuwi/react-hooks';
|
||||
import { BlockToTime } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import Close from './Close.js';
|
||||
import Voting from './Voting.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
imageHash: Hash;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
prime?: string | null;
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Proposal ({ className = '', imageHash, isMember, members, prime, type }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const derive = useCall<DeriveCollectiveProposal>(api.derive[type as 'technicalCommittee'].proposal, [imageHash]);
|
||||
const { hasFailed, isCloseable, isVoteable, remainingBlocks } = useVotingStatus(derive?.votes, members.length, type);
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
|
||||
if (!modLocation || !derive?.votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ayes, end, index, nays, threshold } = derive.votes;
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<Table.Column.Id value={index} />
|
||||
<ProposalCell
|
||||
imageHash={imageHash}
|
||||
isCollective
|
||||
proposal={derive.proposal}
|
||||
/>
|
||||
<td className='number'>
|
||||
{formatNumber(ayes.length)}/{formatNumber(threshold)}
|
||||
</td>
|
||||
<td className='number together'>
|
||||
{remainingBlocks && end && (
|
||||
<>
|
||||
<BlockToTime value={remainingBlocks} />
|
||||
#{formatNumber(end)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className='address'>
|
||||
{ayes.map((address, index): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={`${index}:${address.toHex()}`}
|
||||
value={address}
|
||||
withBalance={false}
|
||||
/>
|
||||
))}
|
||||
</td>
|
||||
<td className='address'>
|
||||
{nays.map((address, index): React.ReactNode => (
|
||||
<AddressMini
|
||||
key={`${index}:${address.toHex()}`}
|
||||
value={address}
|
||||
withBalance={false}
|
||||
/>
|
||||
))}
|
||||
</td>
|
||||
<td className='button'>
|
||||
{isVoteable && !isCloseable && (
|
||||
<Voting
|
||||
hash={imageHash}
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
prime={prime}
|
||||
proposalId={index}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
{isCloseable && (
|
||||
<Close
|
||||
hasFailed={hasFailed}
|
||||
hash={imageHash}
|
||||
idNumber={index}
|
||||
proposal={derive.proposal}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposal);
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic, SubmittableExtrinsicFunction } from '@pezkuwi/api/types';
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputNumber, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useCollectiveInstance, useModal } from '@pezkuwi/react-hooks';
|
||||
import { Extrinsic } from '@pezkuwi/react-params';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
defaultThreshold?: number;
|
||||
defaultValue?: SubmittableExtrinsicFunction<'promise'>;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
interface ProposalState {
|
||||
proposal?: SubmittableExtrinsic<'promise'> | null;
|
||||
proposalLength: number;
|
||||
}
|
||||
|
||||
// TODO We probably want to pull this from config
|
||||
const DEFAULT_THRESHOLD = 1 / 2;
|
||||
|
||||
function Propose ({ defaultThreshold = DEFAULT_THRESHOLD, defaultValue, filter, isMember, members, type }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiDefaultTxSudo } = useApi();
|
||||
const { isOpen, onClose, onOpen } = useModal();
|
||||
const [accountId, setAcountId] = useState<string | null>(null);
|
||||
const [{ proposal, proposalLength }, setProposal] = useState<ProposalState>({ proposalLength: 0 });
|
||||
const [[threshold, hasThreshold], setThreshold] = useState<[BN | null, boolean]>([
|
||||
new BN(Math.min(members.length, (members.length * defaultThreshold) + 1)),
|
||||
true
|
||||
]);
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
|
||||
const _hasThreshold = useCallback(
|
||||
(threshold?: BN | null): boolean =>
|
||||
!!threshold && !threshold.isZero() && threshold.lten(members.length),
|
||||
[members]
|
||||
);
|
||||
|
||||
const _onChangeExtrinsic = useCallback(
|
||||
(proposal?: SubmittableExtrinsic<'promise'>): void => setProposal({
|
||||
proposal,
|
||||
proposalLength: proposal?.length || 0
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const _onChangeThreshold = useCallback(
|
||||
(threshold?: BN): void => setThreshold([threshold || null, _hasThreshold(threshold)]),
|
||||
[_hasThreshold]
|
||||
);
|
||||
|
||||
if (!modLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
header={t('Propose a committee motion')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
filter={members}
|
||||
label={t('propose from account')}
|
||||
onChange={setAcountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
<InputNumber
|
||||
className='medium'
|
||||
isError={!hasThreshold}
|
||||
label={t('threshold')}
|
||||
onChange={_onChangeThreshold}
|
||||
placeholder={t('Positive number between 1 and {{count}}', { replace: { count: members.length } })}
|
||||
value={threshold || undefined}
|
||||
/>
|
||||
<Extrinsic
|
||||
defaultValue={defaultValue || apiDefaultTxSudo}
|
||||
filter={filter}
|
||||
label={t('proposal')}
|
||||
onChange={_onChangeExtrinsic}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
isDisabled={!hasThreshold || !proposal}
|
||||
onStart={onClose}
|
||||
params={
|
||||
api.tx[modLocation].propose.meta.args.length === 3
|
||||
? [threshold, proposal, proposalLength]
|
||||
: [threshold, proposal]
|
||||
}
|
||||
tx={api.tx[modLocation].propose}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!isMember}
|
||||
label={t('Submit proposal')}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Propose);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, MarkWarning, Modal, TxButton, VoteAccount } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useCollectiveInstance, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
hash: Hash | string;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
prime?: string | null;
|
||||
proposalId: BN | number;
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Voting ({ hash, isMember, members, prime, proposalId, type }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [isVotingOpen, toggleVoting] = useToggle();
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
|
||||
if (!modLocation || !hasAccounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVotingOpen && (
|
||||
<Modal
|
||||
header={t('Vote on proposal')}
|
||||
onClose={toggleVoting}
|
||||
size='small'
|
||||
>
|
||||
<Modal.Content>
|
||||
<VoteAccount
|
||||
filter={members}
|
||||
onChange={setAccountId}
|
||||
/>
|
||||
{accountId === prime && (
|
||||
<MarkWarning content={t('You are voting with this collective\'s prime account. The vote will be the default outcome in case of any abstentions.')} />
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='ban'
|
||||
label={t('Vote Nay')}
|
||||
onStart={toggleVoting}
|
||||
params={[hash, proposalId, false]}
|
||||
tx={api.tx[modLocation].vote}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label={t('Vote Aye')}
|
||||
onStart={toggleVoting}
|
||||
params={[hash, proposalId, true]}
|
||||
tx={api.tx[modLocation].vote}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='check'
|
||||
isDisabled={!isMember}
|
||||
label={t('Vote')}
|
||||
onClick={toggleVoting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Voting);
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { ComponentProps } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Button, Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Proposal from './Proposal.js';
|
||||
import Propose from './Propose.js';
|
||||
|
||||
interface Props extends ComponentProps {
|
||||
defaultProposal?: SubmittableExtrinsicFunction<'promise'>;
|
||||
defaultThreshold?: number;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
}
|
||||
|
||||
function Proposals ({ className = '', defaultProposal, defaultThreshold, filter, isMember, members, prime, proposalHashes, type }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<[React.ReactNode?, string?, number?][]>([
|
||||
[t('proposals'), 'start', 2],
|
||||
[t('threshold')],
|
||||
[t('voting end')],
|
||||
[t('aye'), 'address'],
|
||||
[t('nay'), 'address'],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button.Group>
|
||||
<Propose
|
||||
defaultThreshold={defaultThreshold}
|
||||
defaultValue={defaultProposal}
|
||||
filter={filter}
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
type={type}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Table
|
||||
empty={proposalHashes && t('No committee proposals')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{proposalHashes?.map((hash: Hash): React.ReactNode => (
|
||||
<Proposal
|
||||
imageHash={hash}
|
||||
isMember={isMember}
|
||||
key={hash.toHex()}
|
||||
members={members}
|
||||
prime={prime}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Proposals);
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import App from './App.js';
|
||||
|
||||
export { default as useCounter } from './useCounter.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function TechComm ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<App
|
||||
basePath={basePath}
|
||||
className={className}
|
||||
type='technicalCommittee'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TechComm);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm 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-tech-comm');
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@pezkuwi/react-hooks/types';
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
export interface ComponentProps {
|
||||
className?: string;
|
||||
isMember: boolean;
|
||||
prime?: string | null;
|
||||
proposalHashes?: Hash[];
|
||||
members: string[];
|
||||
type: CollectiveType;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
const OPT = {
|
||||
transform: (proposals: Hash[]) => proposals.length
|
||||
};
|
||||
|
||||
function useCounterImpl (): number {
|
||||
const { api, isApiReady } = useApi();
|
||||
const counter = useCall<number>(isApiReady && api.derive.technicalCommittee?.proposals, undefined, OPT) || 0;
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
export default createNamedHook('useCounter', useCounterImpl);
|
||||
Reference in New Issue
Block a user