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
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Group, IFavoriteChainProps, IFavoriteChainsStorage } from './types.js';
import React, { useCallback, useMemo } from 'react';
import { Icon, styled } from '@pezkuwi/react-components';
import Network from './Network.js';
import { getContrastingColor, isFavoriteChain } from './utils.js';
interface Props {
affinities: Record<string, string>;
apiUrl: string;
children?: React.ReactNode;
className?: string;
index: number;
isSelected: boolean;
favoriteChains: IFavoriteChainsStorage,
toggleFavoriteChain: (chainInfo: IFavoriteChainProps) => void;
setApiUrl: (network: string, apiUrl: string) => void;
setGroup: (groupIndex: number) => void;
value: Group;
highlightColor: string;
}
function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, highlightColor, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement<Props> {
const _setGroup = useCallback(
() => setGroup(isSelected ? -1 : index),
[index, isSelected, setGroup]
);
const isFavoriteHeader = useMemo(() => header?.toString().includes('Favorite'), [header]);
const filtered = useMemo(
() => networks.filter(({ isUnreachable }) => !isUnreachable),
[networks]
);
if (isFavoriteHeader && Object.keys(favoriteChains).length === 0) {
return <></>;
}
return (
<StyledDiv
className={`${className}${isSelected ? ' isSelected' : ''}`}
highlightColor={highlightColor}
>
<div
className={`groupHeader${isSpaced ? ' isSpaced' : ''}${isFavoriteHeader ? ' isFavoriteHeader' : ''}`}
onClick={_setGroup}
>
<Icon icon={isSelected ? 'caret-up' : 'caret-down'} />
{header}
</div>
{isSelected && (
<>
<div className='groupNetworks'>
{filtered.map((network, index): React.ReactNode => (
<Network
affinity={affinities[network.name]}
apiUrl={apiUrl}
isFavorite={isFavoriteChain(favoriteChains, {
chainName: network.name,
paraId: network.paraId,
relay: network.nameRelay
})}
key={index}
setApiUrl={setApiUrl}
toggleFavoriteChain={toggleFavoriteChain}
value={network}
/>
))}
</div>
{children}
</>
)}
</StyledDiv>
);
}
const StyledDiv = styled.div<{ highlightColor: string; }>`
.groupHeader {
border-radius: 0.25rem;
cursor: pointer;
line-height: 1;
padding: 0.75rem 1rem;
position: relative;
text-transform: uppercase;
&:hover {
background: var(--bg-table);
}
&.isSpaced {
margin-top: 0.75rem;
}
.ui--Icon {
margin-right: 0.5rem;
}
&.isFavoriteHeader {
&:hover {
background: linear-gradient(
135deg,
${(props) => props.highlightColor}f2 0%,
${(props) => props.highlightColor}99 100%
);
color: ${(props) => getContrastingColor(props.highlightColor)};
}
&::after {
content: '⭐';
margin-left: 8px;
font-size: 16px;
}
}
}
.groupNetworks {
padding: 0.25rem 0 0.5rem 1rem;
}
`;
export default React.memo(GroupDisplay);
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IFavoriteChainProps, Network } from './types.js';
import React, { useCallback, useMemo } from 'react';
import { ChainImg, Dropdown, Icon, styled } from '@pezkuwi/react-components';
import { useTranslation } from '../translate.js';
interface Props {
affinity?: string; // unused - previous selection
apiUrl: string;
className?: string;
setApiUrl: (network: string, apiUrl: string) => void;
value: Network;
isFavorite: boolean;
toggleFavoriteChain: (chainInfo: IFavoriteChainProps) => void;
}
function NetworkDisplay ({ apiUrl, className = '', isFavorite, setApiUrl, toggleFavoriteChain, value: { isChild, isRelay, isUnreachable, name, nameRelay: relay, paraId, providers, ui } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const isSelected = useMemo(
() => providers.some(({ url }) => url === apiUrl),
[apiUrl, providers]
);
const providersOptions = useMemo(() => {
return providers.map(({ name, url }) => ({
text: name,
value: url
}));
}, [providers]);
const _selectUrl = useCallback(
() => {
const filteredProviders = providers.filter(({ url }) => !url.startsWith('light://'));
if (filteredProviders.length === 0) {
alert('No WebSocket (wss://) provider available');
return;
}
return setApiUrl(name, filteredProviders[Math.floor(Math.random() * filteredProviders.length)].url);
},
[name, providers, setApiUrl]
);
const _setApiUrl = useCallback(
(apiUrl: string) => setApiUrl(name, apiUrl),
[name, setApiUrl]
);
const _toggleFavoriteChain = useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
toggleFavoriteChain({ chainName: name, paraId, relay });
}, [name, paraId, relay, toggleFavoriteChain]);
return (
<StyledDiv className={`${className}${isSelected ? ' isSelected highlight--border' : ''}${isUnreachable ? ' isUnreachable' : ''}`}>
<div
className={`markFavoriteSection${isChild ? ' isChild' : ''}`}
onClick={isUnreachable ? undefined : _selectUrl}
>
<div className='endpointSection'>
<ChainImg
className='endpointIcon'
isInline
logo={ui.logo || 'empty'}
withoutHl
/>
<div className='endpointValue'>
<div>{name}</div>
{isSelected && (isRelay || !!paraId) && (
<div className='endpointExtra'>
{isRelay
? t('Relay chain')
: paraId && paraId < 1000
? t('{{relay}} System', { replace: { relay } })
: paraId && paraId < 2000
? t('{{relay}} Common', { replace: { relay } })
: t('{{relay}} Teyrchain', { replace: { relay } })
}
</div>
)}
</div>
</div>
<Icon
className={isFavorite ? 'isFavorite' : ''}
icon='star'
onClick={_toggleFavoriteChain}
/>
</div>
{isSelected &&
<Dropdown
className='isSmall'
onChange={_setApiUrl}
options={providersOptions}
value={apiUrl}
withLabel={false}
/>
}
</StyledDiv>
);
}
const StyledDiv = styled.div`
border-left: 0.25rem solid transparent;
border-radius: 0.25rem;
cursor: pointer;
margin: 0 0 0.25rem 0;
padding: 0.375rem 0.5rem 0.375rem 1rem;
position: relative;
&.isUnreachable {
opacity: var(--opacity-light);
}
&.isSelected {
.markFavoriteSection {
gap: 1rem;
padding-bottom: 1rem;
}
}
.markFavoriteSection {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
&:hover .ui--Icon {
opacity: 0.5;
}
.ui--Icon {
scale: 1.1;
opacity: 0;
transition: color 0.2s ease;
&:hover {
opacity: 0.5;
stroke: darkorange;
color: darkorange;
}
&.isFavorite {
opacity: 1;
stroke: darkorange;
color: darkorange;
}
}
}
&.isSelected,
&:hover {
background: var(--bg-table);
}
.endpointSection {
align-items: center;
display: flex;
justify-content: flex-start;
position: relative;
&+.ui--Toggle {
margin-top: 1rem;
}
&+.endpointProvider {
margin-top: -0.125rem;
}
.endpointValue {
.endpointExtra {
font-size: var(--font-size-small);
opacity: var(--opacity-light);
}
}
}
// we jiggle our labels somewhat...
label {
font-size: var(--font-size-small);
font-weight: var(--font-weight-normal);
text-transform: none;
}
`;
export default React.memo(NetworkDisplay);
+446
View File
@@ -0,0 +1,446 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { LinkOption } from '@pezkuwi/apps-config/endpoints/types';
import type { Group, IFavoriteChainProps } from './types.js';
// ok, this seems to be an eslint bug, this _is_ a package import
import punycode from 'punycode/';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import store from 'store';
import { createWsEndpoints, CUSTOM_ENDPOINT_KEY } from '@pezkuwi/apps-config';
import { Button, Input, Sidebar, styled } from '@pezkuwi/react-components';
import { defaultHighlight } from '@pezkuwi/react-components/styles';
import { useApi } from '@pezkuwi/react-hooks';
import { settings } from '@pezkuwi/ui-settings';
import { isAscii } from '@pezkuwi/util';
import { useTranslation } from '../translate.js';
import GroupDisplay from './Group.js';
import { getFavoriteChains, isFavoriteChain, toggleFavoriteChain } from './utils.js';
interface Props {
className?: string;
offset?: number | string;
onClose: () => void;
}
interface UrlState {
apiUrl: string;
groupIndex: number;
hasUrlChanged: boolean;
isUrlValid: boolean;
}
const STORAGE_AFFINITIES = 'network:affinities';
function isValidUrl (url: string): boolean {
return (
// some random length... we probably want to parse via some lib
(url.length >= 7) &&
// check that it starts with a valid ws identifier
(url.startsWith('ws://') || url.startsWith('wss://') || url.startsWith('light://'))
);
}
function combineEndpoints (endpoints: LinkOption[]): Group[] {
const favoriteChains = getFavoriteChains();
let favoriteGroupIndex = -1;
const combinedEndpoints = endpoints.reduce((result: Group[], e): Group[] => {
if (e.isHeader) {
const isFavoriteHeader =
typeof e.text === 'string' && e.text.includes('Favorite chains');
result.push({ header: e.text, isDevelopment: e.isDevelopment, isSpaced: e.isSpaced, networks: [] });
if (isFavoriteHeader) {
favoriteGroupIndex = result.length - 1;
}
} else {
const prev = result[result.length - 1];
const prov = { isLightClient: e.isLightClient, name: e.textBy, url: e.value };
const isFavorite = isFavoriteChain(favoriteChains,
{ chainName: e.text?.toString() ?? '',
paraId: e.paraId,
relay: e.textRelay?.toString() });
if (isFavorite && favoriteGroupIndex !== -1 && !e.isUnreachable) {
const favGroup = result[favoriteGroupIndex];
const lastFav = favGroup.networks[favGroup.networks.length - 1];
if (lastFav && lastFav.name === e.text && lastFav.nameRelay === e.textRelay && lastFav.paraId === e.paraId) {
lastFav.providers.push(prov);
} else {
favGroup.networks.push({
isChild: e.isChild,
isRelay: !!e.genesisHash,
name: e.text as string,
nameRelay: e.textRelay as string,
paraId: e.paraId,
providers: [prov],
ui: e.ui
});
}
}
if (prev.networks[prev.networks.length - 1] && e.text === prev.networks[prev.networks.length - 1].name) {
prev.networks[prev.networks.length - 1].providers.push(prov);
} else if (!e.isUnreachable) {
prev.networks.push({
isChild: e.isChild,
isRelay: !!e.genesisHash,
name: e.text as string,
nameRelay: e.textRelay as string,
paraId: e.paraId,
providers: [prov],
ui: e.ui
});
}
}
return result;
}, []);
// Swap first two items in `networks` if first item is relay chain
combinedEndpoints.forEach((r) => {
if (r.networks.length >= 2 && r.networks[0].isRelay && r.header?.toString().includes('teyrchains')) {
[r.networks[0], r.networks[1]] = [r.networks[1], r.networks[0]];
}
});
return combinedEndpoints;
}
function getCustomEndpoints (): string[] {
try {
const storedAsset = localStorage.getItem(CUSTOM_ENDPOINT_KEY);
if (storedAsset) {
return JSON.parse(storedAsset) as string[];
}
} catch (e) {
console.error(e);
// ignore error
}
return [];
}
function extractUrlState (apiUrl: string, groups: Group[]): UrlState {
let groupIndex = groups.findIndex(({ networks }) =>
networks.some(({ providers }) =>
providers.some(({ url }) => url === apiUrl)
)
);
if (groupIndex === -1) {
groupIndex = groups.findIndex(({ isDevelopment }) => isDevelopment);
}
return {
apiUrl,
groupIndex,
hasUrlChanged: settings.get().apiUrl !== apiUrl,
isUrlValid: isValidUrl(apiUrl)
};
}
function loadAffinities (groups: Group[]): Record<string, string> {
return Object
.entries<string>(store.get(STORAGE_AFFINITIES) as Record<string, string> || {})
.filter(([network, apiUrl]) =>
groups.some(({ networks }) =>
networks.some(({ name, providers }) =>
name === network && providers.some(({ url }) => url === apiUrl)
)
)
)
.reduce((result: Record<string, string>, [network, apiUrl]): Record<string, string> => ({
...result,
[network]: apiUrl
}), {});
}
function isSwitchDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid: boolean, isLocalFork?: boolean): boolean {
if (!hasUrlChanged) {
if (isLocalFork) {
return false;
} else {
return true;
}
} else if (apiUrl.startsWith('light://')) {
return false;
} else if (isUrlValid) {
return false;
}
return true;
}
function isLocalForkDisabled (hasUrlChanged: boolean, apiUrl: string, isUrlValid: boolean, isLocalFork?: boolean): boolean {
if (!hasUrlChanged) {
if (isLocalFork) {
return true;
} else {
return false;
}
} else if (apiUrl.startsWith('light://')) {
return true;
} else if (isUrlValid) {
return false;
}
return true;
}
function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const linkOptions = createWsEndpoints(t);
const { apiEndpoint, isLocalFork } = useApi();
const [favoriteChains, setFavoriteChains] = useState(() => getFavoriteChains());
const [groups, setGroups] = useState(() => combineEndpoints(linkOptions));
const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>(() => extractUrlState(settings.get().apiUrl, groups));
const [storedCustomEndpoints, setStoredCustomEndpoints] = useState<string[]>(() => getCustomEndpoints());
const [affinities, setAffinities] = useState(() => loadAffinities(groups));
const sidebarRef = useRef<HTMLDivElement>(null);
const isKnownUrl = useMemo(() => {
let result = false;
linkOptions.some((endpoint) => {
if (endpoint.value === apiUrl) {
result = true;
return true;
}
return false;
});
return result;
}, [apiUrl, linkOptions]);
const isSavedCustomEndpoint = useMemo(() => {
let result = false;
storedCustomEndpoints.some((endpoint) => {
if (endpoint === apiUrl) {
result = true;
return true;
}
return false;
});
return result;
}, [apiUrl, storedCustomEndpoints]);
const _changeGroup = useCallback(
(groupIndex: number) => setApiUrl((state) => ({ ...state, groupIndex })),
[]
);
const _toggleFavoriteChain = useCallback((chainInfo: IFavoriteChainProps) => {
toggleFavoriteChain(chainInfo);
setFavoriteChains(getFavoriteChains());
setGroups(combineEndpoints(createWsEndpoints(t)));
}, [t]);
const _removeApiEndpoint = useCallback(
(): void => {
if (!isSavedCustomEndpoint) {
return;
}
const newStoredCurstomEndpoints = storedCustomEndpoints.filter((url) => url !== apiUrl);
try {
localStorage.setItem(CUSTOM_ENDPOINT_KEY, JSON.stringify(newStoredCurstomEndpoints));
setGroups(combineEndpoints(createWsEndpoints(t)));
setStoredCustomEndpoints(getCustomEndpoints());
} catch (e) {
console.error(e);
// ignore error
}
},
[apiUrl, isSavedCustomEndpoint, storedCustomEndpoints, t]
);
const _setApiUrl = useCallback(
(network: string, apiUrl: string): void => {
setAffinities((affinities): Record<string, string> => {
const newValue = { ...affinities, [network]: apiUrl };
store.set(STORAGE_AFFINITIES, newValue);
return newValue;
});
setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex }));
},
[groups]
);
const _onChangeCustom = useCallback(
(apiUrl: string): void => {
if (!isAscii(apiUrl)) {
apiUrl = punycode.toASCII(apiUrl);
}
setApiUrl((state) => ({ ...extractUrlState(apiUrl, groups), groupIndex: state.groupIndex }));
},
[groups]
);
const _onApply = useCallback(
(): void => {
store.set('localFork', '');
settings.set({ ...(settings.get()), apiUrl });
window.location.assign(`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(apiUrl)}${window.location.hash}`);
if (!hasUrlChanged) {
window.location.reload();
}
onClose();
},
[apiUrl, onClose, hasUrlChanged]
);
const _onLocalFork = useCallback(
(): void => {
store.set('localFork', apiUrl);
settings.set({ ...(settings.get()), apiUrl });
window.location.assign(`${window.location.origin}${window.location.pathname}?rpc=${encodeURIComponent(apiUrl)}${window.location.hash}`);
if (!hasUrlChanged) {
window.location.reload();
}
onClose();
},
[apiUrl, onClose, hasUrlChanged]
);
const _saveApiEndpoint = useCallback(
(): void => {
try {
localStorage.setItem(CUSTOM_ENDPOINT_KEY, JSON.stringify([...storedCustomEndpoints, apiUrl]));
_onApply();
} catch (e) {
console.error(e);
// ignore error
}
},
[_onApply, apiUrl, storedCustomEndpoints]
);
const canSwitch = useMemo(
() => isSwitchDisabled(hasUrlChanged, apiUrl, isUrlValid, isLocalFork),
[hasUrlChanged, apiUrl, isUrlValid, isLocalFork]
);
const canLocalFork = useMemo(
() => isLocalForkDisabled(hasUrlChanged, apiUrl, isUrlValid, isLocalFork),
[hasUrlChanged, apiUrl, isUrlValid, isLocalFork]
);
return (
<StyledSidebar
buttons={
<>
<Button
icon='code-fork'
isDisabled={canLocalFork}
label={t('Fork Locally')}
onClick={_onLocalFork}
tooltip='fork-locally-btn'
/>
<Button
icon='sync'
isDisabled={canSwitch}
label={t('Switch')}
onClick={_onApply}
/>
</>
}
className={className}
offset={offset}
onClose={onClose}
position='left'
sidebarRef={sidebarRef}
>
{groups.map((group, index): React.ReactNode => (
<GroupDisplay
affinities={affinities}
apiUrl={apiUrl}
favoriteChains={favoriteChains}
highlightColor={apiEndpoint?.ui.color || defaultHighlight}
index={index}
isSelected={groupIndex === index}
key={index}
setApiUrl={_setApiUrl}
setGroup={_changeGroup}
toggleFavoriteChain={_toggleFavoriteChain}
value={group}
>
{group.isDevelopment && (
<div className='endpointCustomWrapper'>
<Input
className='endpointCustom'
isError={!isUrlValid}
isFull
label={t('custom endpoint')}
onChange={_onChangeCustom}
value={apiUrl}
/>
{isSavedCustomEndpoint
? (
<Button
className='customButton'
icon='trash-alt'
onClick={_removeApiEndpoint}
/>
)
: (
<Button
className='customButton'
icon='save'
isDisabled={!isUrlValid || isKnownUrl}
onClick={_saveApiEndpoint}
/>
)
}
</div>
)}
</GroupDisplay>
))}
</StyledSidebar>
);
}
const StyledSidebar = styled(Sidebar)`
color: var(--color-text);
padding-top: 3.5rem;
.customButton {
position: absolute;
top: 1rem;
right: 1rem;
}
.endpointCustom {
input {
padding-right: 4rem;
}
}
.endpointCustomWrapper {
position: relative;
}
`;
export default React.memo(Endpoints);
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type React from 'react';
export interface IFavoriteChainProps {
chainName: string;
relay?: string;
paraId?: number;
}
export type IFavoriteChainsStorage = Record<string, {relay: string, paraId: number}[]>
export interface Network {
isChild?: boolean;
isLightClient?: boolean;
isRelay?: boolean;
isUnreachable?: boolean;
name: string;
nameRelay?: string;
paraId?: number;
providers: {
name: string;
url: string;
}[];
ui: {
color?: string;
logo?: string;
}
}
export interface Group {
header: React.ReactNode;
isDevelopment?: boolean;
isSpaced?: boolean;
networks: Network[];
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2017-2025 @pezkuwi/apps authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { IFavoriteChainProps, IFavoriteChainsStorage } from './types.js';
import { createWsEndpoints } from '@pezkuwi/apps-config';
export const FAVORITE_CHAINS_KEY = 'pezkuwi-app-favorite-chains';
const chainsConfig = createWsEndpoints((k, v) => v?.toString() || k);
export const toggleFavoriteChain = (
chainInfo: IFavoriteChainProps
) => {
try {
const chainName = chainInfo.chainName;
const meta = { paraId: chainInfo.paraId ?? -1, relay: chainInfo.relay ?? 'Unknown' };
const favoriteChains = getFavoriteChains();
const existingEntries = favoriteChains[chainName] ?? [];
const alreadyExists = existingEntries.some(
(entry) => entry.relay === meta.relay && entry.paraId === meta.paraId
);
let updatedEntries: IFavoriteChainsStorage[string];
if (alreadyExists) {
// Remove the matching entry
updatedEntries = existingEntries.filter(
(entry) => entry.relay !== meta.relay || entry.paraId !== meta.paraId
);
} else {
// Add new entry
updatedEntries = [...existingEntries, meta];
}
const updatedChains: IFavoriteChainsStorage = { ...favoriteChains };
if (updatedEntries.length === 0) {
delete updatedChains[chainName];
} else {
updatedChains[chainName] = updatedEntries;
}
localStorage.setItem(FAVORITE_CHAINS_KEY, JSON.stringify(updatedChains));
} catch {}
};
export const getFavoriteChains = (): IFavoriteChainsStorage => {
try {
const favoriteChains = localStorage.getItem(FAVORITE_CHAINS_KEY);
if (!favoriteChains) {
return {};
}
const parsed: unknown = JSON.parse(favoriteChains);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('Invalid favorite chains format');
}
const result: IFavoriteChainsStorage = {};
for (const [key, value] of Object.entries(parsed) as [string, IFavoriteChainsStorage[string]][]) {
if (!Array.isArray(value)) {
throw new Error(`Invalid value for key "${key}": not an array`);
}
const allValid = value.every(
(entry: IFavoriteChainsStorage[string][number]) =>
typeof entry === 'object' &&
entry !== null &&
typeof entry.relay === 'string' &&
typeof entry.paraId === 'number'
);
if (!allValid) {
throw new Error(`Invalid entries under key "${key}"`);
}
// Make sure key exists in chain config
if (chainsConfig.find((e) => e.text === key)) {
// Make sure "relay" value also exists in chain config
const matchingFavorites = value.filter((v) => v.relay === 'Unknown' ? true : !!chainsConfig.find((e) => e.text === v.relay));
if (matchingFavorites.length > 0) {
result[key] = matchingFavorites;
}
}
}
// Set filtered result to localStorage
localStorage.setItem(FAVORITE_CHAINS_KEY, JSON.stringify(result));
return result;
} catch (e) {
console.error('Failed to parse favorite chains:', e);
localStorage.removeItem(FAVORITE_CHAINS_KEY);
return {};
}
};
export const isFavoriteChain = (
favoriteChains: IFavoriteChainsStorage,
chainInfo: IFavoriteChainProps
): boolean => {
try {
const chainName = chainInfo.chainName;
const meta = { paraId: chainInfo.paraId ?? -1, relay: chainInfo.relay ?? 'Unknown' };
const list = favoriteChains[chainName];
if (!Array.isArray(list)) {
return false;
}
return list.some(
(entry) =>
entry.relay === meta.relay && entry.paraId === meta.paraId
);
} catch (e) {
console.error('Failed to check favorite chain:', e);
return false;
}
};
export function getContrastingColor (hexColor: string): string {
if (typeof hexColor !== 'string') {
return '#000000';
}
let hex = hexColor.replace('#', '').trim();
if (hex.length === 3) {
hex = hex.split('').map((c) => c + c).join('');
}
if (hex.length !== 6 || /[^0-9a-f]/i.test(hex)) {
return '#000000';
}
try {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? '#000000' : '#FFFFFF';
} catch {
return '#000000';
}
}