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
+302
View File
@@ -0,0 +1,302 @@
// Copyright 2017-2025 @pezkuwi/app-poll authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Approvals, Balance, BlockNumber } from '@pezkuwi/types/interfaces';
import type { ITuple } from '@pezkuwi/types/types';
import React, { useEffect, useRef, useState } from 'react';
import { Trans } from 'react-i18next';
import { Button, Columar, InputAddress, Progress, Spinner, styled, Tabs, Toggle, TxButton } from '@pezkuwi/react-components';
import { useApi, useBestNumber, useCallMulti } from '@pezkuwi/react-hooks';
import { BlockToTime, FormatBalance } from '@pezkuwi/react-query';
import { BN, BN_MILLION, BN_ONE, BN_ZERO, bnMax, formatBalance, formatNumber } from '@pezkuwi/util';
import { useTranslation } from './translate.js';
interface Props {
basePath: string;
className?: string;
}
type MultiResult = [Balance | undefined, [Balance, Balance, Balance, Balance] | undefined];
interface Turnout {
percentage: number;
voted: BN;
}
const OPT_MULTI = {
defaultValue: [undefined, undefined] as MultiResult
};
function PollApp ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const bestNumber = useBestNumber();
const [totalIssuance, totals] = useCallMulti<MultiResult>([
api.query.balances?.totalIssuance,
api.query.poll.totals
], OPT_MULTI);
const [accountId, setAccountId] = useState<string | null>(null);
const [turnout, setTurnout] = useState<Turnout | null>(null);
const [opt10m, setOpt10m] = useState(false);
const [opt100m, setOpt100m] = useState(false);
const [opt1b, setOpt1b] = useState(false);
const [opt10b, setOpt10b] = useState(false);
const [progress, setProgress] = useState<BN[] | undefined>();
const itemsRef = useRef([{
isRoot: true,
name: 'poll',
text: t('Denomination poll')
}]);
useEffect((): void => {
if (totalIssuance && totals) {
const max = bnMax(BN_ONE, ...totals);
setProgress(totals.map((total) => total.mul(BN_MILLION).div(max)));
api.query.poll.voteOf
.entries<ITuple<[Approvals, Balance]>>()
.then((entries): void => {
const voted = entries.reduce((voted: BN, [, [, balance]]) => voted.iadd(balance), new BN(0));
const percentage = voted.muln(10_000).div(totalIssuance).toNumber() / 100;
setTurnout({ percentage, voted });
})
.catch(console.log);
}
}, [api, totalIssuance, totals]);
if (!totals || !progress || !bestNumber) {
return (
<main className={className}>
<div className='pollContainer'>
<Spinner label={t('Retrieving totals...')} />
</div>
</main>
);
}
const blocksLeft = (api.consts.poll.end as BlockNumber).sub(bestNumber);
const canVote = blocksLeft.gt(BN_ZERO);
const options: [string, string, boolean, (value: boolean) => void][] = [
[t('No change'), t('No change from the original 2017 sale definitions; will mean a total of 10 million HEZ from genesis.'), opt10m, setOpt10m],
[t('Split of 10x'), t('Split of 10x from the original sale; will mean a total of 100 million HEZ from genesis. Apparent HEZ price would be 10x lower and apparent account balances 10x higher.'), opt100m, setOpt100m],
[t('Split of 100x'), t('Split of 100x from the original sale; will mean a total of 1 billion HEZ from genesis. Apparent HEZ price would be 100x lower and apparent account balances 100x higher.'), opt1b, setOpt1b],
[t('Split of 1000x'), t('Split of 1000x from the original sale; will mean a total of 10 billion HEZ from genesis. Apparent HEZ price would be 1000x lower and apparent account balances 1000x higher.'), opt10b, setOpt10b]
];
const hasValue = opt10m || opt100m || opt1b || opt10b;
/* eslint-disable react/jsx-max-props-per-line */
return (
<StyledMain className={className}>
<Tabs
basePath={basePath}
items={itemsRef.current}
/>
<div className='pollContainer'>
<div className='pollHeader'>
<h1>{t('denomination vote')}</h1>
<div className='pollBlocksRight'>
{turnout && (
<div>
<div>{t('{{balance}} voted', { replace: { balance: formatBalance(turnout.voted) } })}</div>
<div>{t('{{percentage}}% turnout', { replace: { percentage: turnout.percentage.toFixed(2) } })}</div>
</div>
)}
<div>
{canVote
? <BlockToTime value={blocksLeft} />
: t('Completed')
}
<div>#{formatNumber(api.consts.poll.end as BlockNumber)}</div>
</div>
</div>
</div>
<article className='keepAlive'>
<p><Trans key='poll1'>The Pezkuwi HEZ denomination vote: Seventy-two hours after the HEZ token becomes transferable, the most popular option from this poll will decide the denomination used for the HEZ token.</Trans></p>
<p><Trans key='poll2'>This is an <a href='https://en.wikipedia.org/wiki/Approval_voting' rel='noreferrer' target='_blank'>approval vote</a>. There are four options and you may select any combination of them. The most popular of the four will be selected as the final HEZ denomination three days after HEZ token transfers are enabled.</Trans></p>
<p><Trans key='poll3'>Please see the <a href='https://medium.com/pezkuwi-network/the-first-pezkuwi-vote-1fc1b8bd357b' rel='noreferrer' target='_blank'>Medium article </a> for more information</Trans></p>
{canVote && (
<p className='pollAll'><Trans key='poll4'><b>Please vote for any combination of options</b></Trans></p>
)}
<div className={`options ${canVote ? 'canVote' : ''}`}>
{options.map(([label, desc, value, onChange], index) =>
<Columar
is60
key={index}
>
<Columar.Column className='option'>
<div className='optionName'>{label}</div>
<div className='optionDesc'>{desc}</div>
{canVote && (
<Toggle
className='pollToggle'
isDisabled={!canVote}
label={
canVote
? value
? t('Aye, I support this')
: t('Nay, I do not support this')
: t('Voting closed')
}
onChange={onChange}
value={canVote && value}
/>
)}
</Columar.Column>
<Columar.Column>
{totals[index].isZero()
? <div className='result' />
: (
<div className='result'>
<FormatBalance value={totals[index]} />
<Progress
isDisabled={!turnout}
total={turnout?.voted}
value={totals[index]}
/>
</div>
)
}
</Columar.Column>
</Columar>
)}
</div>
{canVote && (
<>
<InputAddress
label={t('vote using my account')}
onChange={setAccountId}
type='account'
/>
<Button.Group>
<TxButton
accountId={accountId}
icon='paper-plane'
isDisabled={!hasValue}
label={t('Vote')}
params={[[opt10m, opt100m, opt1b, opt10b]]}
tx={api.tx.poll.vote}
/>
</Button.Group>
</>
)}
</article>
<div className='pollActions'>
<ul>
<li>{t('Any combination of the four options may be approved of by the voter. There is no need to select only one option!')}</li>
<li>{t('Approving of all or none of the options is equivalent and will not affect the outcome of the poll.')}</li>
<li>{t('All voters may alter their votes any number of times prior to the close of the poll.')}</li>
<li>{t('Voting costs nothing other than the transaction fee and can be done from all accounts with a non-zero spendable balance.')}</li>
<li>{t('Locked funds (e.g. for staking) are counted.')}</li>
<li>{t('No discretionary lock-voting is in place; all HEZ used to vote counts the same.')}</li>
<li>{t('Voting is made on a per-account basis; a single account must all vote the same way and cannot split its vote.')}</li>
<li>{t('This vote does not affect any economics of the Pezkuwi platform. Staking rewards, inflation, effective market capitalisation and the underlying balances of every account remain completely unchanged. It is "merely" about what units we use to denominate the balances into "HEZ" for the purpose of display.')}</li>
</ul>
</div>
</div>
</StyledMain>
);
}
const StyledMain = styled.main`
.pollActions {
opacity: 0.75;
}
.pollAll {
margin-bottom: 0;
padding: 0.75rem 1rem;
text-align: center;
}
.pollBlocksRight {
position: absolute;
right: 0;
text-align: right;
opacity: 0.75;
bottom: 0;
> div {
display: inline-block;
padding: 0 0.75rem;
&+div {
border-left: 1px solid #bbb;
}
}
}
.pollContainer {
margin: 2rem auto;
max-width: 60rem;
}
.pollHeader {
position: relative;
}
.options {
margin: 1rem 0;
.ui--Columar {
margin: 0 -1.25rem;
padding: 0 1.25rem;
&:nth-child(odd) {
background: #f9f8f7;
}
.ui--Column {
padding: 1rem 1.5rem;
}
}
.optionName {
font-size: 1.2rem;
font-weight: var(--font-weight-normal);
line-height: 1;
margin-bottom: 0.75rem;
}
.pollToggle {
margin-top: 0.5rem;
text-align: right;
}
&:not(.canVote) {
.ui--Toggle {
opacity: 0;
.toggle {
display: none;
}
}
}
}
.result {
align-items: center;
display: flex;
justify-content: flex-end;
margin: 0;
text-align: right;
.ui--FormatBalance {
font-size: 1.2rem;
font-weight: var(--font-weight-normal);
line-height: 1;
}
.ui--Progress {
margin: 0.75rem;
}
}
`;
export default React.memo(PollApp);
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/app-poll 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-poll');
}