Mark Chains as Favorites (#11718)
* feat: added star icon for chain * feat: add favorite chains header and localization support * fix: star ux flow enhanced * feat: mark chains as favorite and store it * feat: added transitions and shown already favorite chains * fix: remove favorite chains key from localStorage on error * feat: integrate favorite chains functionality in Group * chore: fix spacing between group headers * feat: enhance favorite chains functionality with structured storage and utility functions * fix: prevent rendering empty component for groups with no favorite chains * chore: translations * chore: match favorite network with relay and paraId * chore: remove logging
This commit is contained in:
parent
930d9cf152
commit
c907fde127
@ -25,6 +25,15 @@ function defaultT (keyOrText: string, text?: string | TOptions, options?: TOptio
|
|||||||
export function createWsEndpoints (t: TFunction = defaultT, firstOnly = false, withSort = true): LinkOption[] {
|
export function createWsEndpoints (t: TFunction = defaultT, firstOnly = false, withSort = true): LinkOption[] {
|
||||||
return [
|
return [
|
||||||
...createCustom(t),
|
...createCustom(t),
|
||||||
|
{
|
||||||
|
isDisabled: false,
|
||||||
|
isHeader: true,
|
||||||
|
isSpaced: true,
|
||||||
|
text: t('rpc.header.favorite', 'Favorite chains', { ns: 'apps-config' }),
|
||||||
|
textBy: '',
|
||||||
|
ui: {},
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
@ -38,6 +47,7 @@ export function createWsEndpoints (t: TFunction = defaultT, firstOnly = false, w
|
|||||||
{
|
{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
|
isSpaced: true,
|
||||||
text: t('rpc.header.kusama.relay', 'Kusama & parachains', { ns: 'apps-config' }),
|
text: t('rpc.header.kusama.relay', 'Kusama & parachains', { ns: 'apps-config' }),
|
||||||
textBy: '',
|
textBy: '',
|
||||||
ui: {},
|
ui: {},
|
||||||
@ -77,6 +87,7 @@ export function createWsEndpoints (t: TFunction = defaultT, firstOnly = false, w
|
|||||||
{
|
{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
|
isSpaced: true,
|
||||||
text: t('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
|
text: t('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
|
||||||
textBy: '',
|
textBy: '',
|
||||||
ui: {},
|
ui: {},
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"rpc.dev.custom.own": "Custom",
|
"rpc.dev.custom.own": "Custom",
|
||||||
"rpc.dev.local": "Local Node",
|
"rpc.dev.local": "Local Node",
|
||||||
"rpc.header.dev": "Development",
|
"rpc.header.dev": "Development",
|
||||||
|
"rpc.header.favorite": "Favorite chains",
|
||||||
"rpc.header.kusama.relay": "Kusama & parachains",
|
"rpc.header.kusama.relay": "Kusama & parachains",
|
||||||
"rpc.header.live": "Live networks",
|
"rpc.header.live": "Live networks",
|
||||||
"rpc.header.paseo.relay": "Test Paseo & parachains",
|
"rpc.header.paseo.relay": "Test Paseo & parachains",
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import type { Group } from './types.js';
|
import type { Group, IFavoriteChainProps, IFavoriteChainsStorage } from './types.js';
|
||||||
|
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { Icon, styled } from '@polkadot/react-components';
|
import { Icon, styled } from '@polkadot/react-components';
|
||||||
|
|
||||||
import Network from './Network.js';
|
import Network from './Network.js';
|
||||||
|
import { isFavoriteChain } from './utils.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
affinities: Record<string, string>;
|
affinities: Record<string, string>;
|
||||||
@ -16,12 +17,14 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
index: number;
|
index: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
favoriteChains: IFavoriteChainsStorage,
|
||||||
|
toggleFavoriteChain: (chainInfo: IFavoriteChainProps) => void;
|
||||||
setApiUrl: (network: string, apiUrl: string) => void;
|
setApiUrl: (network: string, apiUrl: string) => void;
|
||||||
setGroup: (groupIndex: number) => void;
|
setGroup: (groupIndex: number) => void;
|
||||||
value: Group;
|
value: Group;
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupDisplay ({ affinities, apiUrl, children, className = '', index, isSelected, setApiUrl, setGroup, value: { header, isSpaced, networks } }: Props): React.ReactElement<Props> {
|
function GroupDisplay ({ affinities, apiUrl, children, className = '', favoriteChains, index, isSelected, setApiUrl, setGroup, toggleFavoriteChain, value: { header, isSpaced, networks } }: Props): React.ReactElement<Props> {
|
||||||
const _setGroup = useCallback(
|
const _setGroup = useCallback(
|
||||||
() => setGroup(isSelected ? -1 : index),
|
() => setGroup(isSelected ? -1 : index),
|
||||||
[index, isSelected, setGroup]
|
[index, isSelected, setGroup]
|
||||||
@ -32,6 +35,10 @@ function GroupDisplay ({ affinities, apiUrl, children, className = '', index, is
|
|||||||
[networks]
|
[networks]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (header?.toString().includes('Favorite') && Object.keys(favoriteChains).length === 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv className={`${className}${isSelected ? ' isSelected' : ''}`}>
|
<StyledDiv className={`${className}${isSelected ? ' isSelected' : ''}`}>
|
||||||
<div
|
<div
|
||||||
@ -48,8 +55,14 @@ function GroupDisplay ({ affinities, apiUrl, children, className = '', index, is
|
|||||||
<Network
|
<Network
|
||||||
affinity={affinities[network.name]}
|
affinity={affinities[network.name]}
|
||||||
apiUrl={apiUrl}
|
apiUrl={apiUrl}
|
||||||
|
isFavorite={isFavoriteChain(favoriteChains, {
|
||||||
|
chainName: network.name,
|
||||||
|
paraId: network.paraId,
|
||||||
|
relay: network.nameRelay
|
||||||
|
})}
|
||||||
key={index}
|
key={index}
|
||||||
setApiUrl={setApiUrl}
|
setApiUrl={setApiUrl}
|
||||||
|
toggleFavoriteChain={toggleFavoriteChain}
|
||||||
value={network}
|
value={network}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import type { Network } from './types.js';
|
import type { IFavoriteChainProps, Network } from './types.js';
|
||||||
|
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { ChainImg, styled } from '@polkadot/react-components';
|
import { ChainImg, Icon, styled } from '@polkadot/react-components';
|
||||||
|
|
||||||
import { useTranslation } from '../translate.js';
|
import { useTranslation } from '../translate.js';
|
||||||
import Url from './Url.js';
|
import Url from './Url.js';
|
||||||
@ -16,9 +16,11 @@ interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
setApiUrl: (network: string, apiUrl: string) => void;
|
setApiUrl: (network: string, apiUrl: string) => void;
|
||||||
value: Network;
|
value: Network;
|
||||||
|
isFavorite: boolean;
|
||||||
|
toggleFavoriteChain: (chainInfo: IFavoriteChainProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NetworkDisplay ({ apiUrl, className = '', setApiUrl, value: { isChild, isRelay, isUnreachable, name, nameRelay: relay, paraId, providers, ui } }: Props): React.ReactElement<Props> {
|
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 { t } = useTranslation();
|
||||||
const isSelected = useMemo(
|
const isSelected = useMemo(
|
||||||
() => providers.some(({ url }) => url === apiUrl),
|
() => providers.some(({ url }) => url === apiUrl),
|
||||||
@ -39,33 +41,46 @@ function NetworkDisplay ({ apiUrl, className = '', setApiUrl, value: { isChild,
|
|||||||
[name, setApiUrl]
|
[name, setApiUrl]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const _toggleFavoriteChain = useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFavoriteChain({ chainName: name, paraId, relay });
|
||||||
|
}, [name, paraId, relay, toggleFavoriteChain]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDiv className={`${className}${isSelected ? ' isSelected highlight--border' : ''}${isUnreachable ? ' isUnreachable' : ''}`}>
|
<StyledDiv className={`${className}${isSelected ? ' isSelected highlight--border' : ''}${isUnreachable ? ' isUnreachable' : ''}`}>
|
||||||
<div
|
<div
|
||||||
className={`endpointSection${isChild ? ' isChild' : ''}`}
|
className={`markFavoriteSection${isChild ? ' isChild' : ''}`}
|
||||||
onClick={isUnreachable ? undefined : _selectUrl}
|
onClick={isUnreachable ? undefined : _selectUrl}
|
||||||
>
|
>
|
||||||
<ChainImg
|
<div className='endpointSection'>
|
||||||
className='endpointIcon'
|
<ChainImg
|
||||||
isInline
|
className='endpointIcon'
|
||||||
logo={ui.logo || 'empty'}
|
isInline
|
||||||
withoutHl
|
logo={ui.logo || 'empty'}
|
||||||
/>
|
withoutHl
|
||||||
<div className='endpointValue'>
|
/>
|
||||||
<div>{name}</div>
|
<div className='endpointValue'>
|
||||||
{isSelected && (isRelay || !!paraId) && (
|
<div>{name}</div>
|
||||||
<div className='endpointExtra'>
|
{isSelected && (isRelay || !!paraId) && (
|
||||||
{isRelay
|
<div className='endpointExtra'>
|
||||||
? t('Relay chain')
|
{isRelay
|
||||||
: paraId && paraId < 1000
|
? t('Relay chain')
|
||||||
? t('{{relay}} System', { replace: { relay } })
|
: paraId && paraId < 1000
|
||||||
: paraId && paraId < 2000
|
? t('{{relay}} System', { replace: { relay } })
|
||||||
? t('{{relay}} Common', { replace: { relay } })
|
: paraId && paraId < 2000
|
||||||
: t('{{relay}} Parachain', { replace: { relay } })
|
? t('{{relay}} Common', { replace: { relay } })
|
||||||
}
|
: t('{{relay}} Parachain', { replace: { relay } })
|
||||||
</div>
|
}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Icon
|
||||||
|
className={isFavorite ? 'isFavorite' : ''}
|
||||||
|
icon='star'
|
||||||
|
onClick={_toggleFavoriteChain}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && providers.map(({ name, url }): React.ReactNode => (
|
{isSelected && providers.map(({ name, url }): React.ReactNode => (
|
||||||
<Url
|
<Url
|
||||||
@ -92,6 +107,42 @@ const StyledDiv = styled.div`
|
|||||||
opacity: var(--opacity-light);
|
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,
|
&.isSelected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--bg-table);
|
background: var(--bg-table);
|
||||||
@ -107,10 +158,6 @@ const StyledDiv = styled.div`
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.isChild .endpointIcon {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&+.endpointProvider {
|
&+.endpointProvider {
|
||||||
margin-top: -0.125rem;
|
margin-top: -0.125rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
|
import type { LinkOption } from '@polkadot/apps-config/endpoints/types';
|
||||||
import type { Group } from './types.js';
|
import type { Group, IFavoriteChainProps } from './types.js';
|
||||||
|
|
||||||
// ok, this seems to be an eslint bug, this _is_ a package import
|
// ok, this seems to be an eslint bug, this _is_ a package import
|
||||||
import punycode from 'punycode/';
|
import punycode from 'punycode/';
|
||||||
@ -17,6 +17,7 @@ import { isAscii } from '@polkadot/util';
|
|||||||
|
|
||||||
import { useTranslation } from '../translate.js';
|
import { useTranslation } from '../translate.js';
|
||||||
import GroupDisplay from './Group.js';
|
import GroupDisplay from './Group.js';
|
||||||
|
import { getFavoriteChains, isFavoriteChain, toggleFavoriteChain } from './utils.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -43,13 +44,47 @@ function isValidUrl (url: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function combineEndpoints (endpoints: LinkOption[]): Group[] {
|
function combineEndpoints (endpoints: LinkOption[]): Group[] {
|
||||||
|
const favoriteChains = getFavoriteChains();
|
||||||
|
let favoriteGroupIndex = -1;
|
||||||
|
|
||||||
return endpoints.reduce((result: Group[], e): Group[] => {
|
return endpoints.reduce((result: Group[], e): Group[] => {
|
||||||
if (e.isHeader) {
|
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: [] });
|
result.push({ header: e.text, isDevelopment: e.isDevelopment, isSpaced: e.isSpaced, networks: [] });
|
||||||
|
|
||||||
|
if (isFavoriteHeader) {
|
||||||
|
favoriteGroupIndex = result.length - 1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const prev = result[result.length - 1];
|
const prev = result[result.length - 1];
|
||||||
const prov = { isLightClient: e.isLightClient, name: e.textBy, url: e.value };
|
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) {
|
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);
|
prev.networks[prev.networks.length - 1].providers.push(prov);
|
||||||
} else if (!e.isUnreachable) {
|
} else if (!e.isUnreachable) {
|
||||||
@ -155,6 +190,7 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const linkOptions = createWsEndpoints(t);
|
const linkOptions = createWsEndpoints(t);
|
||||||
const { isLocalFork } = useApi();
|
const { isLocalFork } = useApi();
|
||||||
|
const [favoriteChains, setFavoriteChains] = useState(() => getFavoriteChains());
|
||||||
const [groups, setGroups] = useState(() => combineEndpoints(linkOptions));
|
const [groups, setGroups] = useState(() => combineEndpoints(linkOptions));
|
||||||
const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>(() => extractUrlState(settings.get().apiUrl, groups));
|
const [{ apiUrl, groupIndex, hasUrlChanged, isUrlValid }, setApiUrl] = useState<UrlState>(() => extractUrlState(settings.get().apiUrl, groups));
|
||||||
const [storedCustomEndpoints, setStoredCustomEndpoints] = useState<string[]>(() => getCustomEndpoints());
|
const [storedCustomEndpoints, setStoredCustomEndpoints] = useState<string[]>(() => getCustomEndpoints());
|
||||||
@ -198,6 +234,12 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const _toggleFavoriteChain = useCallback((chainInfo: IFavoriteChainProps) => {
|
||||||
|
toggleFavoriteChain(chainInfo);
|
||||||
|
setFavoriteChains(getFavoriteChains());
|
||||||
|
setGroups(combineEndpoints(createWsEndpoints(t)));
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
const _removeApiEndpoint = useCallback(
|
const _removeApiEndpoint = useCallback(
|
||||||
(): void => {
|
(): void => {
|
||||||
if (!isSavedCustomEndpoint) {
|
if (!isSavedCustomEndpoint) {
|
||||||
@ -325,11 +367,13 @@ function Endpoints ({ className = '', offset, onClose }: Props): React.ReactElem
|
|||||||
<GroupDisplay
|
<GroupDisplay
|
||||||
affinities={affinities}
|
affinities={affinities}
|
||||||
apiUrl={apiUrl}
|
apiUrl={apiUrl}
|
||||||
|
favoriteChains={favoriteChains}
|
||||||
index={index}
|
index={index}
|
||||||
isSelected={groupIndex === index}
|
isSelected={groupIndex === index}
|
||||||
key={index}
|
key={index}
|
||||||
setApiUrl={_setApiUrl}
|
setApiUrl={_setApiUrl}
|
||||||
setGroup={_changeGroup}
|
setGroup={_changeGroup}
|
||||||
|
toggleFavoriteChain={_toggleFavoriteChain}
|
||||||
value={group}
|
value={group}
|
||||||
>
|
>
|
||||||
{group.isDevelopment && (
|
{group.isDevelopment && (
|
||||||
|
|||||||
@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
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 {
|
export interface Network {
|
||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
isLightClient?: boolean;
|
isLightClient?: boolean;
|
||||||
|
|||||||
114
packages/apps/src/Endpoints/utils.ts
Normal file
114
packages/apps/src/Endpoints/utils.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2017-2025 @polkadot/apps authors & contributors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import type { IFavoriteChainProps, IFavoriteChainsStorage } from './types.js';
|
||||||
|
|
||||||
|
export const FAVORITE_CHAINS_KEY = 'polkadot-app-favorite-chains';
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
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}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[key] = value as IFavoriteChainsStorage[string];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -16,7 +16,7 @@ interface Props {
|
|||||||
icon: IconName;
|
icon: IconName;
|
||||||
isPadded?: boolean;
|
isPadded?: boolean;
|
||||||
isSpinning?: boolean;
|
isSpinning?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => void;
|
||||||
size?: '1x' | '2x';
|
size?: '1x' | '2x';
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user