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
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RuntimeVersion } from '@pezkuwi/types/interfaces';
import React from 'react';
import { ChainImg, Icon, styled } from '@pezkuwi/react-components';
import { useApi, useCall, useIpfs, useToggle } from '@pezkuwi/react-hooks';
import { BestNumber, Chain } from '@pezkuwi/react-query';
import Endpoints from '../Endpoints/index.js';
interface Props {
className?: string;
}
function ChainInfo ({ className }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
const runtimeVersion = useCall<RuntimeVersion>(isApiReady && api.rpc.state.subscribeRuntimeVersion);
const { ipnsChain } = useIpfs();
const [isEndpointsVisible, toggleEndpoints] = useToggle();
const canToggle = !ipnsChain;
return (
<StyledDiv className={className}>
<div
className={`apps--SideBar-logo-inner${canToggle ? ' isClickable' : ''} highlight--color-contrast`}
onClick={toggleEndpoints}
>
<ChainImg />
<div className='info media--1000'>
<Chain className='chain' />
{runtimeVersion && (
<div className='runtimeVersion'>{runtimeVersion.specName.toString()}/{runtimeVersion.specVersion.toNumber()}</div>
)}
<BestNumber
className='bestNumber'
label='#'
/>
</div>
{canToggle && (
<Icon
className='dropdown'
icon={isEndpointsVisible ? 'caret-right' : 'caret-down'}
/>
)}
</div>
{isEndpointsVisible && (
<Endpoints onClose={toggleEndpoints} />
)}
</StyledDiv>
);
}
const StyledDiv = styled.div`
box-sizing: border-box;
padding: 0.5rem 1rem 0.5rem 0;
margin: 0;
.apps--SideBar-logo-inner {
display: flex;
align-items: center;
justify-content: space-between;
&.isClickable {
cursor: pointer;
}
.ui--ChainImg {
height: 3rem;
margin-right: 0.5rem;
width: 3rem;
}
.ui--Icon.dropdown,
> div.info {
text-align: right;
vertical-align: middle;
}
.ui--Icon.dropdown {
flex: 0;
margin: 0;
width: 1rem;
}
.info {
flex: 1;
font-size: var(--font-size-tiny);
line-height: 1.2;
padding-right: 0.5rem;
text-align: right;
.chain {
font-size: var(--font-size-small);
max-width: 16rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.runtimeVersion {
letter-spacing: -0.01em;
}
}
}
`;
export default React.memo(ChainInfo);
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Group } from './types.js';
import React from 'react';
import { Icon, styled } from '@pezkuwi/react-components';
import Item from './Item.js';
interface Props extends Group {
className?: string;
isActive: boolean;
}
const SHA_COL = 'rgba(34, 36, 38, 0.12)';
const SHA_OFF = '5px';
function Grouping ({ className = '', isActive, name, routes }: Props): React.ReactElement<Props> {
if (routes.length === 1 && routes[0].group === 'settings') {
return (
<Item
className={isActive ? 'isActive' : ''}
classNameText='smallHide'
isToplevel
route={routes[0]}
/>
);
}
return (
<StyledLi className={`${className} ${isActive ? 'isActive' : ''}`}>
<div className={`groupHdr ${!isActive ? 'highlight--color-contrast' : ''}`}>
<span className='smallHide'>{name}</span>
<Icon
className='smallShow'
icon={routes[0].icon}
/>
<Icon icon='caret-down' />
</div>
<ul className='groupMenu'>
{routes.map((route): React.ReactNode => (
<Item
key={route.name}
route={route}
/>
))}
</ul>
</StyledLi>
);
}
const StyledLi = styled.li`
cursor: pointer;
position: relative;
.groupHdr {
border-radius: 0.25rem;
padding: 0.857rem 1.375rem;
font-weight: var(--font-weight-normal);
line-height: 1.214rem;
> .ui--Icon {
margin-left: 0.75rem;
}
}
&.isActive .groupHdr {
background-color: var(--bg-tabs);
font-weight: var(--font-weight-normal);
margin-bottom: 0;
}
.groupMenu {
border-radius: 0.25rem;
box-shadow: 0 ${SHA_OFF} ${SHA_OFF} -${SHA_OFF} ${SHA_COL}, ${SHA_OFF} 0 ${SHA_OFF} -${SHA_OFF} ${SHA_COL}, -${SHA_OFF} 0 ${SHA_OFF} -${SHA_OFF} ${SHA_COL};
display: none;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
top: 2.9rem;
z-index: 250;
> li {
z-index: 1;
a {
padding-right: 4rem;
}
}
&::before {
bottom: 0;
content: ' ';
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: -1;
}
}
&:hover {
.groupHdr {
box-shadow: 0px 4px 37px rgba(0, 0, 0, 0.08);
padding-bottom: 2rem;
margin-bottom: -2rem;
}
.groupMenu {
display: block;
> li:hover {
background: var(--bg-menu-hover);
}
}
}
`;
export default React.memo(Grouping);
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ItemRoute } from './types.js';
import React from 'react';
import { Badge, Icon, styled } from '@pezkuwi/react-components';
import { useToggle } from '@pezkuwi/react-hooks';
interface Props {
className?: string;
classNameText?: string;
isLink?: boolean;
isToplevel?: boolean;
route: ItemRoute;
}
const DUMMY_COUNTER = () => 0;
function Item ({ className = '', classNameText, isLink, isToplevel, route: { Modal, href, icon, name, text, useCounter = DUMMY_COUNTER } }: Props): React.ReactElement<Props> {
const [isModalVisible, toggleModal] = useToggle();
const count = useCounter();
return (
<StyledLi className={`${className} ui--MenuItem ${count ? 'withCounter' : ''} ${isLink ? 'isLink' : ''} ${isToplevel ? 'topLevel highlight--color-contrast' : ''}`}>
<a
href={Modal ? undefined : (href || `#/${name}`)}
onClick={Modal ? toggleModal : undefined}
rel='noopener noreferrer'
target={href ? '_blank' : undefined}
>
<Icon icon={icon} />
<span className={classNameText}>{text}</span>
{!!count && (
<Badge
color='white'
info={count}
/>
)}
</a>
{Modal && isModalVisible && (
<Modal onClose={toggleModal} />
)}
</StyledLi>
);
}
const StyledLi = styled.li`
cursor: pointer;
position: relative;
white-space: nowrap;
&.topLevel {
font-weight: var(--font-weight-normal);
line-height: 1.214rem;
border-radius: 0.15rem;
a {
padding: 0.857rem 0.857em 0.857rem 1rem;
line-height: 1.214rem;
border-radius: 0.25rem;
}
&.isActive.highlight--color-contrast {
font-weight: var(--font-weight-normal);
color: var(--color-text);
a {
background-color: var(--bg-tabs);
}
}
&.isActive {
border-radius: 0.15rem 0.15rem 0 0;
a {
padding: 0.857rem 1.429rem 0.857rem;
cursor: default;
}
&&.withCounter a {
padding-right: 3.2rem;
}
}
.ui--Badge {
top: 0.7rem;
}
}
&&.withCounter a {
padding-right: 3.2rem;
}
a {
color: inherit !important;
display: block;
padding: 0.5rem 1.15rem 0.57rem;
text-decoration: none;
line-height: 1.5rem;
}
.ui--Badge {
position: absolute;
right: 0.5rem;
}
.ui--Icon {
margin-right: 0.5rem;
}
`;
export default React.memo(Item);
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BareProps as Props } from '@pezkuwi/react-components/types';
import React from 'react';
import { packageInfo } from '@pezkuwi/apps-config';
import { styled } from '@pezkuwi/react-components';
import { useApi } from '@pezkuwi/react-hooks';
import { NodeName, NodeVersion } from '@pezkuwi/react-query';
const appsVersion = `apps v${packageInfo.version.replace('-x', '')}`;
function NodeInfo ({ className = '' }: Props): React.ReactElement<Props> {
const { api, isApiReady } = useApi();
return (
<StyledDiv className={`${className} media--1400 highlight--color-contrast ui--NodeInfo`}>
{isApiReady && (
<div className='node'>
<NodeName />&nbsp;
<NodeVersion label='v' />
</div>
)}
<div>{api.libraryInfo.replace('@pezkuwi/', '')}</div>
<div>{appsVersion}</div>
</StyledDiv>
);
}
const StyledDiv = styled.div`
background: transparent;
font-size: var(--font-size-tiny);
line-height: 1.2;
padding: 0 0 0 1rem;
text-align: right;
> div {
margin-bottom: -0.125em;
> div {
display: inline-block;
}
}
`;
export default React.memo(NodeInfo);
+258
View File
@@ -0,0 +1,258 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Route, Routes } from '@pezkuwi/apps-routing/types';
import type { ApiProps } from '@pezkuwi/react-api/types';
import type { AccountId } from '@pezkuwi/types/interfaces';
import type { Group, Groups, ItemRoute } from './types.js';
import React, { useMemo, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import createRoutes from '@pezkuwi/apps-routing';
import { styled } from '@pezkuwi/react-components';
import { useAccounts, useApi, useCall, useTeleport } from '@pezkuwi/react-hooks';
import { findMissingApis } from '../endpoint.js';
import { useTranslation } from '../translate.js';
import ChainInfo from './ChainInfo.js';
import Grouping from './Grouping.js';
import Item from './Item.js';
import NodeInfo from './NodeInfo.js';
interface Props {
className?: string;
}
function createExternals (t: (key: string, optionsOrText?: string | { replace: Record<string, unknown> }, options?: { ns: string }) => string): ItemRoute[] {
return [
{
href: 'https://github.com/pezkuwi-js/apps',
icon: 'code-branch',
name: 'github',
text: t('nav.github', 'GitHub', { ns: 'apps-routing' })
},
{
href: 'https://wiki.pezkuwi.network',
icon: 'book',
name: 'wiki',
text: t('nav.wiki', 'Wiki', { ns: 'apps-routing' })
}
];
}
function checkVisible ({ api, isApiConnected, isApiReady, isDevelopment: isApiDevelopment }: ApiProps, allowTeleport: boolean, hasAccounts: boolean, hasSudo: boolean, { isDevelopment, isHidden, needsAccounts, needsApi, needsApiCheck, needsApiInstances, needsSudo, needsTeleport }: Route['display']): boolean {
if (isHidden) {
return false;
} else if (needsAccounts && !hasAccounts) {
return false;
} else if (!needsApi) {
return true;
} else if (!isApiReady || !isApiConnected) {
return false;
} else if (needsSudo && !hasSudo) {
return false;
} else if (needsTeleport && !allowTeleport) {
return false;
} else if (!isApiDevelopment && isDevelopment) {
return false;
}
return findMissingApis(api, needsApi, needsApiInstances, needsApiCheck).length === 0;
}
function extractGroups (routing: Routes, groupNames: Record<string, string>, apiProps: ApiProps, allowTeleport: boolean, hasAccounts: boolean, hasSudo: boolean): Group[] {
return Object
.values(
routing.reduce((all: Groups, route): Groups => {
if (!all[route.group]) {
all[route.group] = {
name: groupNames[route.group],
routes: [route]
};
} else {
all[route.group].routes.push(route);
}
return all;
}, {})
)
.map(({ name, routes }): Group => ({
name,
routes: routes.filter(({ display }) =>
checkVisible(apiProps, allowTeleport, hasAccounts, hasSudo, display)
)
}))
.filter(({ routes }) => routes.length);
}
function Menu ({ className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts, hasAccounts } = useAccounts();
const apiProps = useApi();
const { allowTeleport } = useTeleport();
const sudoKey = useCall<AccountId>(apiProps.isApiReady && apiProps.api.query.sudo?.key);
const location = useLocation();
const externalRef = useRef(createExternals(t));
const routeRef = useRef(createRoutes(t));
const groupRef = useRef({
accounts: t('Accounts'),
developer: t('Developer'),
files: t('Files'),
governance: t('Governance'),
network: t('Network'),
settings: t('Settings')
});
const hasSudo = useMemo(
() => !!sudoKey && allAccounts.some((a) => sudoKey.eq(a)),
[allAccounts, sudoKey]
);
const visibleGroups = useMemo(
() => extractGroups(routeRef.current, groupRef.current, apiProps, allowTeleport, hasAccounts, hasSudo),
[allowTeleport, apiProps, hasAccounts, hasSudo]
);
const activeRoute = useMemo(
() => routeRef.current.find(({ name }) =>
location.pathname.startsWith(`/${name}`)
) || null,
[location]
);
return (
<StyledDiv className={`${className}${(!apiProps.isApiReady || !apiProps.isApiConnected) ? ' isLoading' : ''} highlight--bg`}>
<div className='menuContainer'>
<div className='menuSection'>
<ChainInfo />
<ul className='menuItems'>
{visibleGroups.map(({ name, routes }): React.ReactNode => (
<Grouping
isActive={!!activeRoute && activeRoute.group === name.toLowerCase()}
key={name}
name={name}
routes={routes}
/>
))}
</ul>
</div>
<div className='menuSection media--1200'>
<ul className='menuItems'>
{externalRef.current.map((route): React.ReactNode => (
<Item
isLink
isToplevel
key={route.name}
route={route}
/>
))}
</ul>
</div>
<NodeInfo className='media--1400' />
</div>
</StyledDiv>
);
}
const StyledDiv = styled.div`
width: 100%;
padding: 0;
z-index: 220;
position: relative;
.smallShow {
display: none;
}
& .menuContainer {
flex-direction: row;
align-items: center;
display: flex;
justify-content: space-between;
padding: 0 1.5rem;
width: 100%;
max-width: var(--width-full);
margin: 0 auto;
}
&.isLoading {
background: #999 !important;
.menuActive {
background: var(--bg-page);
}
&:before {
filter: grayscale(1);
}
.menuItems {
filter: grayscale(1);
}
}
.menuSection {
align-items: center;
display: flex;
}
.menuActive {
background: var(--bg-tabs);
border-bottom: none;
border-radius: 0.25rem 0.25rem 0 0;
color: var(--color-text);
padding: 1rem 1.5rem;
margin: 0 1rem -1px;
z-index: 1;
.ui--Icon {
margin-right: 0.5rem;
}
}
.menuItems {
flex: 1 1;
list-style: none;
margin: 0 1rem 0 0;
padding: 0;
> li {
display: inline-block;
}
> li + li {
margin-left: 0.375rem
}
}
.ui--NodeInfo {
align-self: center;
}
@media only screen and (max-width: 800px) {
.groupHdr {
padding: 0.857rem 0.75rem;
}
.smallShow {
display: initial;
}
.smallHide {
display: none;
}
.menuItems {
margin-right: 0;
> li + li {
margin-left: 0.25rem;
}
}
}
`;
export default React.memo(Menu);
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import type React from 'react';
import type { Routes } from '@pezkuwi/apps-routing/types';
export interface ItemRoute {
Modal?: React.ComponentType<any>;
href?: string;
icon: IconName;
name: string;
text: string;
useCounter?: () => number | string | null;
}
export interface Group {
name: string;
routes: Routes;
}
export type Groups = Record<string, Group>;