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
+88
View File
@@ -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'>&nbsp;</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);
+25
View File
@@ -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);
+8
View File
@@ -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');
}
+14
View File
@@ -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;
}
+19
View File
@@ -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);