From 653c4728c8b4af82777d12463160bf20d2901718 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 4 Mar 2024 16:49:45 +0000 Subject: [PATCH 01/39] Add mainnet token pairs --- config/mainnet.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/config/mainnet.ts b/config/mainnet.ts index 20df9e3f..14d4712a 100644 --- a/config/mainnet.ts +++ b/config/mainnet.ts @@ -84,6 +84,9 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { "adorable-quaint-bellatrix": {}, "honorable-steel-rasalhague": { hub: "elated-tan-skat" + }, + "green-giddy-denebola": { + hub: "elated-tan-skat" } } } @@ -185,6 +188,9 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { }, 'honorable-steel-rasalhague': { wrapper: '0xa5274efA35EbeFF47C1510529D9a8812F95F5735' + }, + 'green-giddy-denebola': { + wrapper: '0xa5274efA35EbeFF47C1510529D9a8812F95F5735' } } } @@ -305,6 +311,10 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { mainnet: { clone: true, hub: 'elated-tan-skat' + }, + "green-giddy-denebola": { + clone: true, + hub: "elated-tan-skat" } } } @@ -369,6 +379,24 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { } }, "green-giddy-denebola": { // nebula connections + eth: { + eth: { + address: '0xaB01BAd2C86e24D371A13eD6367bdCa819589C5D', + chains: { + 'elated-tan-skat': { + clone: true + }, + mainnet: { + clone: true, + hub: 'elated-tan-skat' + }, + "honorable-steel-rasalhague": { + clone: true, + hub: "elated-tan-skat" + } + } + } + }, erc20: { skl: { address: "0x7F73B66d4e6e67bCdeaF277b9962addcDabBFC4d", From e83def73ac54ed853daaf4ce0fd8ec0a1bc0c0de Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 22 Jul 2024 15:48:36 +0100 Subject: [PATCH 02/39] Update connect-src in vercel.json --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index a4d981c7..d72891e8 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "headers": [ { "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" + "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' http://eth-node.skalenodes.com https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" }, { "key": "X-Content-Type-Options", @@ -20,4 +20,4 @@ "destination": "/" } ] -} \ No newline at end of file +} From 2115490dcd71d32bc87052788de28aea846ff63f Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 26 Jul 2024 18:28:04 +0100 Subject: [PATCH 03/39] Update CSR for Google Tag Manager --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index d72891e8..2a980a56 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "headers": [ { "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' http://eth-node.skalenodes.com https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" + "value": "default-src 'none'; script-src 'self' 'sha256-9kFihJ0ZOM+GgbS9s6zRngAhk2HvawY3s9ThnvanBVU=' https://www.googletagmanager.com 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' www.googletagmanager.com * data:; connect-src 'self' www.googletagmanager.com http://eth-node.skalenodes.com https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" }, { "key": "X-Content-Type-Options", From 39be73cc6cedfc6d499c15063c60c79d04742f02 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 7 Aug 2024 13:16:54 +0100 Subject: [PATCH 04/39] Add ecosystem page, update home page, add core package, update interfaces --- .gitignore | 5 +- build.sh | 6 +- bun.lockb | Bin 382398 -> 385095 bytes config/mainnet.ts | 4 +- package.json | 12 +- packages/core/bun.lockb | Bin 0 -> 4260 bytes packages/core/package.json | 17 ++ packages/core/src/index.ts | 25 ++ packages/core/src/types/ChainsMetadata.ts | 76 +++++ packages/core/src/types/index.ts | 158 +++++++++++ .../core/src/types/staking/Beneficiary.ts | 34 +++ packages/core/src/types/staking/Delegation.ts | 71 +++++ packages/core/src/types/staking/Delegator.ts | 35 +++ .../core/src/types/staking/SkaleContract.ts | 39 +++ packages/core/src/types/staking/Staking.ts | 31 ++ packages/core/src/types/staking/Validator.ts | 48 ++++ packages/core/src/types/staking/index.ts | 28 ++ packages/core/tsconfig.json | 15 + src/App.scss | 62 +++- src/Router.tsx | 28 +- src/SkDrawer.tsx | 16 +- src/_variables.scss | 10 +- src/components/AppCard.tsx | 7 +- src/components/AppChains.tsx | 10 +- src/components/Breadcrumbs.tsx | 8 +- src/components/CategorySection.tsx | 15 +- src/components/ChainCard.tsx | 10 +- src/components/ChainLogo.tsx | 8 +- src/components/CollapsibleDescription.tsx | 9 +- src/components/FeaturedApps.tsx | 17 +- src/components/HubCard.tsx | 13 +- src/components/HubsSection.tsx | 17 +- src/components/Meson.tsx | 20 +- src/components/MetricsWarning.tsx | 4 +- src/components/PageCard.tsx | 40 ++- src/components/SchainDetails.tsx | 13 +- src/components/TokenSurface.tsx | 5 +- src/components/chains/HubApps.tsx | 9 +- src/components/chains/HubTile.tsx | 12 +- .../chains/tabs/ChainTabsSection.tsx | 5 +- src/components/chains/tabs/DeveloperInfo.tsx | 5 +- src/components/chains/tabs/Tabs.tsx | 13 +- .../delegation/RetrieveRewardModal.tsx | 1 + src/components/ecosystem/AppCardV2.tsx | 90 ++++++ src/components/ecosystem/AppSearch.tsx | 64 +++++ src/components/ecosystem/Categories.tsx | 268 ++++++++++++++++++ src/components/ecosystem/CategoriesShips.tsx | 90 ++++++ src/components/ecosystem/SearchBar.tsx | 85 ++++++ .../ecosystem/SelectedCategories.tsx | 136 +++++++++ src/components/ecosystem/Socials.tsx | 161 +++++++++++ src/components/ecosystem/SubcategoryList.tsx | 72 +++++ src/core/chain.ts | 13 +- src/core/constants.ts | 6 +- src/core/contracts.ts | 11 +- src/core/ecosystem/apps.ts | 89 ++++++ src/core/ecosystem/categories.ts | 95 +++++++ src/core/ecosystem/categoryUtils.ts | 65 +++++ src/core/explorer.ts | 15 +- src/core/metadata.ts | 21 +- src/core/paymaster.ts | 11 +- src/core/transferHistory.ts | 9 +- src/core/types.ts | 160 ----------- src/data/contractsMeta.ts | 4 +- src/pages/App.tsx | 29 +- src/pages/Apps.tsx | 89 ------ src/pages/Bridge.tsx | 16 +- src/pages/Chain.tsx | 17 +- src/pages/Chains.tsx | 13 +- src/pages/Ecosystem.tsx | 190 +++++++++++++ src/pages/Start.tsx | 75 +++-- src/styles/components.scss | 96 +++++++ 71 files changed, 2432 insertions(+), 519 deletions(-) create mode 100755 packages/core/bun.lockb create mode 100644 packages/core/package.json create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/types/ChainsMetadata.ts create mode 100644 packages/core/src/types/index.ts create mode 100644 packages/core/src/types/staking/Beneficiary.ts create mode 100644 packages/core/src/types/staking/Delegation.ts create mode 100644 packages/core/src/types/staking/Delegator.ts create mode 100644 packages/core/src/types/staking/SkaleContract.ts create mode 100644 packages/core/src/types/staking/Staking.ts create mode 100644 packages/core/src/types/staking/Validator.ts create mode 100644 packages/core/src/types/staking/index.ts create mode 100644 packages/core/tsconfig.json create mode 100644 src/components/ecosystem/AppCardV2.tsx create mode 100644 src/components/ecosystem/AppSearch.tsx create mode 100644 src/components/ecosystem/Categories.tsx create mode 100644 src/components/ecosystem/CategoriesShips.tsx create mode 100644 src/components/ecosystem/SearchBar.tsx create mode 100644 src/components/ecosystem/SelectedCategories.tsx create mode 100644 src/components/ecosystem/Socials.tsx create mode 100644 src/components/ecosystem/SubcategoryList.tsx create mode 100644 src/core/ecosystem/apps.ts create mode 100644 src/core/ecosystem/categories.ts create mode 100644 src/core/ecosystem/categoryUtils.ts delete mode 100644 src/core/types.ts delete mode 100644 src/pages/Apps.tsx create mode 100644 src/pages/Ecosystem.tsx create mode 100644 src/styles/components.scss diff --git a/.gitignore b/.gitignore index ba13c4e7..96dac237 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,7 @@ src/metadata/faucet.json .vercel public/sitemap.xml -public/robots.txt \ No newline at end of file +public/robots.txt + +packages/*/node_modules +packages/*/dist \ No newline at end of file diff --git a/build.sh b/build.sh index 675a818f..2a03cf1e 100644 --- a/build.sh +++ b/build.sh @@ -35,5 +35,9 @@ node generate-imports.cjs ./src/assets/validators bash generate_sitemap.sh +echo "Building packages..." +bun run build:packages +bun i + echo "Building..." -bun run build +bun run build:portal diff --git a/bun.lockb b/bun.lockb index 30098777999370015d19a5a799a8299bb15683d0..017b32869d555e8cb6f6a6109e512b1343b125f9 100755 GIT binary patch delta 78037 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. + */ +/** + * @file index.ts + * @copyright SKALE Labs 2024-Present + */ + +import * as types from './types' + +export { types } \ No newline at end of file diff --git a/packages/core/src/types/ChainsMetadata.ts b/packages/core/src/types/ChainsMetadata.ts new file mode 100644 index 00000000..2620a00c --- /dev/null +++ b/packages/core/src/types/ChainsMetadata.ts @@ -0,0 +1,76 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * @file ChainsMetadata.ts + * @copyright SKALE Labs 2024-Present + */ + +import { SkaleNetwork } from '.' + +export interface ChainMetadata { + alias?: string + shortAlias?: string + minSfuelWei?: string + faucetUrl?: string + category: string | string[] + background?: string + gradientBackground?: string + description?: string + url?: string + apps?: AppMetadataMap +} + +export interface AppMetadata { + alias: string + gradientBackground?: string + description?: string + contracts?: string[] + dappradar?: string | boolean + legacy?: boolean + featured?: boolean + social?: AppSocials + tags?: string[] + added?: number + categories: CategoriesMap +} + +export interface AppSocials { + website?: string; + x?: string; + telegram?: string; + github?: string; + discord?: string; + swell?: string; +} + +export interface CategoriesMap { + [category: string]: string[] | null +} + +export interface AppMetadataMap { + [appName: string]: AppMetadata +} + +export interface ChainsMetadataMap { + [chainName: string]: ChainMetadata +} + +export type NetworksMetadataMap = { + [key in SkaleNetwork]: ChainsMetadataMap +} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts new file mode 100644 index 00000000..10a94802 --- /dev/null +++ b/packages/core/src/types/index.ts @@ -0,0 +1,158 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file index.ts + * @copyright SKALE Labs 2024-Present + */ + +export { ChainsMetadataMap, ChainMetadata, AppMetadata, AppMetadataMap, AppSocials, NetworksMetadataMap } from './ChainsMetadata' +export * as staking from './staking' + +export type AddressType = `0x${string}` +export type Size = 'xs' | 'sm' | 'md' | 'lg' +export type SkaleNetwork = 'mainnet' | 'staging' | 'legacy' | 'regression' | 'testnet' + +export type TSChainArray = [ + string, + AddressType, + number, + number, + number, + number, + number, + number, + number, + number, + AddressType, + boolean, + boolean +] + +export interface ISChain { + name: string + mainnetOwner: AddressType + indexInOwnerList: number + partOfNode: number + lifetime: number + startDate: number + startBlock: number + deposit: number + index: number + generation: number + originator: AddressType + multitransactionMode: boolean + thresholdEncryption: boolean +} + +export interface ISChainData { + schain: TSChainArray + nodes: INodeInfo[] +} + +export interface INodeInfo { + id: number + name: string + ip: string + base_port: number + domain: string + schain_base_port: number + httpRpcPort: number + httpsRpcPort: number + wsRpcPort: number + wssRpcPort: number + infoHttpRpcPort: number + http_endpoint_ip: string + https_endpoint_ip: string + ws_endpoint_ip: string + wss_endpoint_ip: string + infoHttp_endpoint_ip: string + http_endpoint_domain: string + https_endpoint_domain: string + ws_endpoint_domain: string + wss_endpoint_domain: string + infoHttp_endpoint_domain: string + block_ts: number +} + +export interface IGasInfo { + LastBlock: string + SafeGasPrice: string + ProposeGasPrice: string + FastGasPrice: string + suggestBaseFee: string + gasUsedRatio: string +} + +export interface IMetrics { + gas: number + last_updated: number + metrics: IMetricsChainMap +} + +export interface IMetricsChainMap { + [chainName: string]: IChainMetrics +} + +export interface IChainMetrics { + chain_stats: any + apps_counters: IAppCountersMap +} + +export interface IAppCountersMap { + [appName: string]: IAppCounters | null +} + +export interface IAppCounters { + [contractAddress: AddressType]: IAddressCounters +} + +export interface IAddressCounters { + gas_usage_count: string + token_transfers_count: string + transactions_count: string + validations_count: string +} + +export interface IStats { + schains_number: number + summary: IStatsMap + schains: { [schainName: string]: IStatsMap } +} + +export interface IStatsMap { + total: IStatsData + total_7d: IStatsData + total_30d: IStatsData + group_by_month: any +} + +export interface IStatsData { + tx_count_total: number + block_count_total: number + gas_total_used: number + gas_fees_total_gwei: number + gas_fees_total_eth: number + gas_fees_total_usd: number + users_count_total: number +} + +export interface IAppId { + app: string + chain: string + totalTransactions?: number +} diff --git a/packages/core/src/types/staking/Beneficiary.ts b/packages/core/src/types/staking/Beneficiary.ts new file mode 100644 index 00000000..bb59c56d --- /dev/null +++ b/packages/core/src/types/staking/Beneficiary.ts @@ -0,0 +1,34 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file Beneficiary.ts + * @copyright SKALE Labs 2023-Present + */ + +import { AddressType } from ".." + +export type IBeneficiaryArray = [bigint, bigint, bigint, bigint, bigint, AddressType] + +export interface IBeneficiary { + status: bigint + planId: bigint + startMonth: bigint + fullAmount: bigint + amountAfterLockup: bigint + requestedAddress: AddressType +} diff --git a/packages/core/src/types/staking/Delegation.ts b/packages/core/src/types/staking/Delegation.ts new file mode 100644 index 00000000..1442120d --- /dev/null +++ b/packages/core/src/types/staking/Delegation.ts @@ -0,0 +1,71 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file Delegation.ts + * @copyright SKALE Labs 2023-Present + */ + +import { AddressType } from ".." + +export type IDelegationArray = [ + AddressType, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + string +] + +export interface IDelegation { + id: bigint + address: AddressType + validator_id: bigint + amount: bigint + delegation_period: bigint + created: bigint + started: bigint + finished: bigint + info: string + stateId: bigint + state: string +} + +export interface IDelegationsToValidator { + validatorId: bigint + delegations: IDelegation[] + rewards: bigint + staked: bigint +} + +export enum DelegationType { + REGULAR = 0, + ESCROW = 1, + ESCROW2 = 2 +} + +export interface IRewardInfo { + validatorId: number + delegationType: DelegationType +} + +export interface IDelegationInfo { + delegationId: bigint + delegationType: DelegationType +} diff --git a/packages/core/src/types/staking/Delegator.ts b/packages/core/src/types/staking/Delegator.ts new file mode 100644 index 00000000..33d81c38 --- /dev/null +++ b/packages/core/src/types/staking/Delegator.ts @@ -0,0 +1,35 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file Delegator.ts + * @copyright SKALE Labs 2024-Present + */ + +import { AddressType } from ".." + +export interface IDelegatorInfo { + balance: bigint + staked: bigint + rewards: bigint + forbiddenToDelegate: bigint + allowedToDelegate?: bigint + vested?: bigint + fullAmount?: bigint + unlocked?: bigint + address: AddressType +} diff --git a/packages/core/src/types/staking/SkaleContract.ts b/packages/core/src/types/staking/SkaleContract.ts new file mode 100644 index 00000000..2d4267b1 --- /dev/null +++ b/packages/core/src/types/staking/SkaleContract.ts @@ -0,0 +1,39 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file SkaleContract.ts + * @copyright SKALE Labs 2024-Present + */ + +import { type Contract } from 'ethers' + +export type SkaleContractName = + | 'delegationController' + | 'skaleToken' + | 'allocator' + | 'distributor' + | 'validatorService' + | 'grantsAllocator' + | 'tokenState' + +export type ISkaleContractsMap = { + [key in SkaleContractName]: Contract +} + +export type ContractType = 'delegation' | 'distributor' diff --git a/packages/core/src/types/staking/Staking.ts b/packages/core/src/types/staking/Staking.ts new file mode 100644 index 00000000..5521306e --- /dev/null +++ b/packages/core/src/types/staking/Staking.ts @@ -0,0 +1,31 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Staking.ts + * @copyright SKALE Labs 2023-Present + */ + +import { type DelegationType, type IDelegationsToValidator, type IDelegatorInfo } from '.' + +export type StakingInfoMap = { [key in DelegationType]: StakingInfo | null } + +export interface StakingInfo { + delegations: IDelegationsToValidator[] + info: IDelegatorInfo +} diff --git a/packages/core/src/types/staking/Validator.ts b/packages/core/src/types/staking/Validator.ts new file mode 100644 index 00000000..72a22d6a --- /dev/null +++ b/packages/core/src/types/staking/Validator.ts @@ -0,0 +1,48 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file Validator.ts + * @copyright SKALE Labs 2024-Present + */ + +import { AddressType } from ".." + +export type IValidatorArray = [ + string, + AddressType, + AddressType, + string, + bigint, + bigint, + bigint, + boolean +] + +export interface IValidator { + name: string + validatorAddress: AddressType + requestedAddress: AddressType + description: string + feeRate: bigint + registrationTime: bigint + minimumDelegationAmount: bigint + acceptNewRequests: boolean + trusted: boolean + id: number + linkedNodes: number +} diff --git a/packages/core/src/types/staking/index.ts b/packages/core/src/types/staking/index.ts new file mode 100644 index 00000000..10279eb4 --- /dev/null +++ b/packages/core/src/types/staking/index.ts @@ -0,0 +1,28 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file index.ts + * @copyright SKALE Labs 2024-Present + */ + +export * from './Beneficiary' +export * from './Delegator' +export * from './Delegation' +export * from './SkaleContract' +export * from './Staking' +export * from './Validator' \ No newline at end of file diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..5c32f7a0 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "node", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} diff --git a/src/App.scss b/src/App.scss index 416b7130..286cedfe 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,4 +1,5 @@ @import './variables'; +@import './styles/components'; :root { background: black; @@ -257,7 +258,6 @@ body::-webkit-scrollbar { } .br__tile { - // height: 150px; border-radius: 25px !important; border: 1px solid #171616 !important; } @@ -433,11 +433,6 @@ body::-webkit-scrollbar { box-shadow: none !important; } - -.outlined { - background: rgba(41, 255, 148, 0.08) -} - .outlinedGray { background: rgb(126 126 126 / 15%); } @@ -720,12 +715,13 @@ input[type=number] { .shipNew { margin-right: 5px; - background: linear-gradient(180deg, #4e2600, #401200); + background: #93B8EC; border-radius: 20px; padding: 3px 6px; p { - color: #fcb381 !important + color: #000000de !important; + font-weight: 600 !important; } } @@ -769,6 +765,16 @@ input[type=number] { } } +.ship_pretge { + background: linear-gradient(180deg, #E7B443, #b1882f); + color: #201808; +} + +.ship_new { + color: rgb(57 218 248); + background: linear-gradient(rgb(20 66 59), rgb(11 36 33)); +} + .ship_DELEGATED { background: linear-gradient(180deg, #0f3d29, #0a2a1c); color: #3cda94; @@ -1186,14 +1192,11 @@ a, height: 17px; } - min-height: 48px !important; - - - + min-height: 45px !important; } .tab.Mui-selected { - background-color: rgba(41, 255, 148, 0.16) !important; + background-color: rgba(147, 184, 236, 0.16) !important; } .MuiTabs-indicator { @@ -1234,12 +1237,28 @@ a, max-width: calc(100vw - 32px); } -.MuiBackdrop-root { - backdrop-filter: blur(3px) !important; +.skPopup { + .MuiBackdrop-root { + backdrop-filter: blur(3px) !important; + } +} + +.socialIcon { + width: 30px !important; + height: 30px !important; + svg { + width: 30px !important; + height: 30px !important; + } } .pSec { color: RGB(255 255 255 / 65%); + + svg { + fill: RGB(255 255 255 / 65%); + } + } .addressInput { @@ -1250,4 +1269,17 @@ a, .Mui-disabled { -webkit-text-fill-color: #ffffff; } +} + + +.bg { + background: $sk-bg !important; +} + +.bgPrim { + background: $sk-bg-prim !important; +} + +.hidden { + display: none; } \ No newline at end of file diff --git a/src/Router.tsx b/src/Router.tsx index d06ca4cd..3d8b5732 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -18,18 +18,19 @@ import { useWagmiSwitchNetwork, walletClientToSigner, enforceNetwork, - type interfaces, cls, cmn } from '@skalenetwork/metaport' +import { type types } from '@/core' + import Bridge from './pages/Bridge' import Faq from './pages/Faq' import Terms from './pages/Terms' import Chains from './pages/Chains' import Chain from './pages/Chain' import Stats from './pages/Stats' -import Apps from './pages/Apps' +import Ecosystem from './pages/Ecosystem' import App from './pages/App' import History from './pages/History' import Portfolio from './pages/Portfolio' @@ -53,8 +54,8 @@ import { getValidators } from './core/delegation/validators' import { initContracts } from './core/contracts' import { getStakingInfoMap } from './core/delegation/staking' import { formatSChains } from './core/chain' -import { IMetrics, ISChain, IStats, IAppId } from './core/types' import { getTopAppsByTransactions } from './core/explorer' + import { loadMeta } from './core/metadata' export default function Router() { @@ -64,11 +65,11 @@ export default function Router() { const theme = useTheme() const isXs = useMediaQuery(theme.breakpoints.down('sm')) - const [chainsMeta, setChainsMeta] = useState(null) - const [schains, setSchains] = useState([]) - const [metrics, setMetrics] = useState(null) - const [topApps, setTopApps] = useState(null) - const [stats, setStats] = useState(null) + const [chainsMeta, setChainsMeta] = useState(null) + const [schains, setSchains] = useState([]) + const [metrics, setMetrics] = useState(null) + const [topApps, setTopApps] = useState(null) + const [stats, setStats] = useState(null) const [termsAccepted, setTermsAccepted] = useState(false) const [stakingTermsAccepted, setStakingTermsAccepted] = useState(false) @@ -77,7 +78,7 @@ export default function Router() { const [validators, setValidators] = useState([]) const [si, setSi] = useState({ 0: null, 1: null, 2: null }) - const [customAddress, setCustomAddress] = useState(undefined) + const [customAddress, setCustomAddress] = useState(undefined) const mpc = useMetaportStore((state: MetaportState) => state.mpc) const transfersHistory = useMetaportStore((state) => state.transfersHistory) @@ -98,7 +99,7 @@ export default function Router() { }, []) useEffect(() => { - setCustomAddress((searchParams.get('_customAddress') as interfaces.AddressType) ?? undefined) + setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) }, [location]) useEffect(() => { @@ -245,7 +246,7 @@ export default function Router() { /> } /> - } /> + } /> } /> @@ -291,7 +292,10 @@ export default function Router() { } /> - } /> + } + /> } /> } /> diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index c33523ee..def2aa18 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -22,6 +22,7 @@ import DonutLargeRoundedIcon from '@mui/icons-material/DonutLargeRounded' import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined' import AddCardRoundedIcon from '@mui/icons-material/AddCardRounded' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' import { DUNE_SKALE_URL } from './core/constants' @@ -113,9 +114,22 @@ export default function SkDrawer() { } > - + + + + + + + + + + +

NEW

diff --git a/src/_variables.scss b/src/_variables.scss index 3880aff4..346e8028 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -1,9 +1,9 @@ $sk-border-radius: 25px; +$sk-bg: #191919; +$sk-bg-prim: #000000; +$sk-btn-height: 47px; +$border-color: #353535; +// legacy $sk-paper-color: rgb(136 135 135 / 15%); $sk-gray-background-color: rgba(161, 161, 161, 0.2); - - -$sk-btn-height: 47px; - -$border-color: rgba(131, 131, 131, 0.20); \ No newline at end of file diff --git a/src/components/AppCard.tsx b/src/components/AppCard.tsx index df488fc9..20cab988 100644 --- a/src/components/AppCard.tsx +++ b/src/components/AppCard.tsx @@ -22,7 +22,8 @@ */ import { Link } from 'react-router-dom' -import { cmn, cls, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' import Button from '@mui/material/Button' import ChainLogo from './ChainLogo' @@ -32,10 +33,10 @@ import { formatNumber } from '../core/timeHelper' import { chainBg, getChainAlias } from '../core/metadata' export default function AppCard(props: { - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork schainName: string appName: string - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap transactions?: number }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) diff --git a/src/components/AppChains.tsx b/src/components/AppChains.tsx index 02d7e5fd..5314a375 100644 --- a/src/components/AppChains.tsx +++ b/src/components/AppChains.tsx @@ -21,18 +21,18 @@ * @copyright SKALE Labs 2024-Present */ -import { cmn, cls, type interfaces, SkPaper, styles } from '@skalenetwork/metaport' +import { cmn, cls, SkPaper, styles } from '@skalenetwork/metaport' +import { type types } from '@/core' import CategorySection from './CategorySection' import appChainsIcon from '../assets/appChains.png' -import { IMetrics } from '../core/types' import { MAINNET_CHAIN_NAME } from '../core/constants' export default function AppChains(props: { - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork schains: any[] - metrics: IMetrics | null - chainsMeta: interfaces.ChainsMetadataMap + metrics: types.IMetrics | null + chainsMeta: types.ChainsMetadataMap isXs: boolean }) { if (props.schains.length === 0) return diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 6e607766..6a3a748f 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -21,10 +21,16 @@ * @copyright SKALE Labs 2024-Present */ +import { ReactElement } from 'react' import Button from '@mui/material/Button' import { Link } from 'react-router-dom' import { cmn, cls } from '@skalenetwork/metaport' -import { BreadcrumbSection } from '../core/types' + +export interface BreadcrumbSection { + icon: ReactElement + text: string + url?: string +} export default function Breadcrumbs(props: { sections: BreadcrumbSection[]; className?: string }) { return ( diff --git a/src/components/CategorySection.tsx b/src/components/CategorySection.tsx index 4ab56c6c..53c6654b 100644 --- a/src/components/CategorySection.tsx +++ b/src/components/CategorySection.tsx @@ -21,22 +21,23 @@ * @copyright SKALE Labs 2022-Present */ +import { getChainAlias } from '@skalenetwork/metaport' +import { type types } from '@/core' + import Box from '@mui/material/Box' import Grid from '@mui/material/Grid' import ChainCard from './ChainCard' -import { type interfaces, getChainAlias } from '@skalenetwork/metaport' -import { IMetrics, ISChain } from '../core/types' export default function CategorySection(props: { schains: any category: string - skaleNetwork: interfaces.SkaleNetwork - chainsMeta: interfaces.ChainsMetadataMap - metrics?: IMetrics | null + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap + metrics?: types.IMetrics | null }) { if (!props.schains || props.schains.length === 0) return - const schains = props.schains.sort((a: ISChain, b: ISChain) => { + const schains = props.schains.sort((a: types.ISChain, b: types.ISChain) => { const aliasA = getChainAlias(props.skaleNetwork, a.name) const aliasB = getChainAlias(props.skaleNetwork, b.name) return aliasA.localeCompare(aliasB) @@ -46,7 +47,7 @@ export default function CategorySection(props: {
- {schains.map((schain: ISChain) => ( + {schains.map((schain: types.ISChain) => (
) diff --git a/src/components/CollapsibleDescription.tsx b/src/components/CollapsibleDescription.tsx index ae1f5c1c..98ff5019 100644 --- a/src/components/CollapsibleDescription.tsx +++ b/src/components/CollapsibleDescription.tsx @@ -27,9 +27,14 @@ import { cls, cmn } from '@skalenetwork/metaport' interface TruncateTextProps { text: string lines?: number + expandable?: boolean } -const CollapsibleDescription: React.FC = ({ text, lines = 2 }) => { +const CollapsibleDescription: React.FC = ({ + text, + lines = 2, + expandable = false +}) => { const [isExpanded, setIsExpanded] = useState(false) const [isTruncated, setIsTruncated] = useState(false) const textRef = useRef(null) @@ -70,7 +75,7 @@ const CollapsibleDescription: React.FC = ({ text, lines = 2 } > {text}

- {isTruncated && ( + {isTruncated && expandable && (

Show {isExpanded ? 'less' : 'more'}

diff --git a/src/components/FeaturedApps.tsx b/src/components/FeaturedApps.tsx index 2d83a008..51e654f5 100644 --- a/src/components/FeaturedApps.tsx +++ b/src/components/FeaturedApps.tsx @@ -21,13 +21,12 @@ */ import { Grid } from '@mui/material' -import { interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' -import AppCard from './AppCard' -import { IAppId } from '../core/types' +import AppCard from './ecosystem/AppCardV2' -function getFeaturedApps(chainsMeta: interfaces.ChainsMetadataMap): IAppId[] { - const featuredApps: IAppId[] = [] +function getFeaturedApps(chainsMeta: types.ChainsMetadataMap): types.IAppId[] { + const featuredApps: types.IAppId[] = [] for (const chain in chainsMeta) { const apps = chainsMeta[chain].apps for (const appKey in apps) { @@ -44,14 +43,14 @@ function getFeaturedApps(chainsMeta: interfaces.ChainsMetadataMap): IAppId[] { } export default function FeaturedApps(props: { - skaleNetwork: interfaces.SkaleNetwork - chainsMeta: interfaces.ChainsMetadataMap + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap }) { const featuredApps = getFeaturedApps(props.chainsMeta) return ( - {featuredApps.map((appId, index) => ( - + {featuredApps.slice(0, 3).map((appId, index) => ( + diff --git a/src/components/HubsSection.tsx b/src/components/HubsSection.tsx index c8506bba..37814578 100644 --- a/src/components/HubsSection.tsx +++ b/src/components/HubsSection.tsx @@ -21,22 +21,23 @@ * @copyright SKALE Labs 2024-Present */ +import { cls, cmn } from '@skalenetwork/metaport' +import { type types } from '@/core' + import Box from '@mui/material/Box' import Grid from '@mui/material/Grid' import HubCard from './HubCard' -import { cls, cmn, type interfaces } from '@skalenetwork/metaport' -import { IMetrics, ISChain } from '../core/types' export default function HubsSection(props: { - schains: ISChain[] - metrics: IMetrics | null + schains: types.ISChain[] + metrics: types.IMetrics | null category: string - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork isXs: boolean - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap }) { - function getMaxLength(schain: string, chainsMeta: interfaces.ChainsMetadataMap): number { + function getMaxLength(schain: string, chainsMeta: types.ChainsMetadataMap): number { return Object.keys(chainsMeta[schain].apps || {}).length } @@ -51,7 +52,7 @@ export default function HubsSection(props: {
- {props.schains.map((schain: ISChain) => ( + {props.schains.map((schain: types.ISChain) => ( (false) - const chainsMeta: interfaces.ChainsMetadataMap = CHAINS_META[props.skaleNetwork] const { address } = useWagmiAccount() - function getChainShortAlias(meta: interfaces.ChainsMetadataMap, name: string): string { + function getChainShortAlias(meta: types.ChainsMetadataMap, name: string): string { return meta[name]?.shortAlias !== undefined ? meta[name].shortAlias! : name } function openMeson(chain: string) { - const shortAlias = getChainShortAlias(chainsMeta, chain) + const shortAlias = getChainShortAlias(props.chainsMeta, chain) const link = `https://meson.to/skale-${shortAlias}/${address}` window.open(link, 'meson.to', 'width=375,height=640') } diff --git a/src/components/MetricsWarning.tsx b/src/components/MetricsWarning.tsx index e6fda091..06eb00c6 100644 --- a/src/components/MetricsWarning.tsx +++ b/src/components/MetricsWarning.tsx @@ -22,17 +22,17 @@ */ import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' import { Container } from '@mui/material' import RestoreRoundedIcon from '@mui/icons-material/RestoreRounded' import Message from './Message' -import { IMetrics } from '../core/types' import { timestampToDate } from '../core/helper' const FOUR_HOURS_IN_SECONDS = 4 * 60 * 60 -export default function MetricsWarning(props: { metrics: IMetrics | null }) { +export default function MetricsWarning(props: { metrics: types.IMetrics | null }) { if (!props.metrics || Date.now() / 1000 - props.metrics.last_updated < FOUR_HOURS_IN_SECONDS) return return ( diff --git a/src/components/PageCard.tsx b/src/components/PageCard.tsx index 38778f12..79d11328 100644 --- a/src/components/PageCard.tsx +++ b/src/components/PageCard.tsx @@ -22,34 +22,26 @@ */ import { Link } from 'react-router-dom' -import { cmn, cls } from '@skalenetwork/metaport' -import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' +import { cmn, cls, SkPaper, styles } from '@skalenetwork/metaport' +import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded' export default function PageCard(props: { name: string; icon: any; description: string }) { return ( -
-
- -
-
-
-
-

- {props.name} -

-
-

{props.description}

-
-
- -
+ + +
+
+
+
{props.icon}
+

{props.name}

+

{props.description}

- -
-
+
+ +
+
+ + ) } diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index d99472c3..09c1f80a 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -32,9 +32,9 @@ import { type MetaportCore, SkPaper, getChainAlias, - chainBg, - type interfaces + chainBg } from '@skalenetwork/metaport' +import { type types } from '@/core' import Button from '@mui/material/Button' import { Container } from '@mui/material' @@ -62,15 +62,14 @@ import SkBtn from './SkBtn' import { MAINNET_CHAIN_LOGOS, MAINNET_CHAIN_NAME } from '../core/constants' import { getRpcUrl, getChainId, HTTPS_PREFIX, getChainDescription } from '../core/chain' import { getExplorerUrl } from '../core/explorer' -import { IChainMetrics, IStatsData } from '../core/types' import { formatNumber } from '../core/timeHelper' import ChainTabsSection from './chains/tabs/ChainTabsSection' export default function SchainDetails(props: { schainName: string - chainsMeta: interfaces.ChainsMetadataMap - schainStats: IStatsData | null - schainMetrics: IChainMetrics | null + chainsMeta: types.ChainsMetadataMap + schainStats: types.IStatsData | null + schainMetrics: types.IChainMetrics | null chain: any mpc: MetaportCore isXs: boolean @@ -190,7 +189,7 @@ export default function SchainDetails(props: { children={

{chainAlias}

- +
} /> diff --git a/src/components/TokenSurface.tsx b/src/components/TokenSurface.tsx index 4a2b3397..8ce70964 100644 --- a/src/components/TokenSurface.tsx +++ b/src/components/TokenSurface.tsx @@ -22,13 +22,14 @@ */ import { useState, useEffect } from 'react' +import { cmn, cls, styles, TokenIcon, ChainIcon, type interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' import { CopyToClipboard } from 'react-copy-to-clipboard' import Tooltip from '@mui/material/Tooltip' import ButtonBase from '@mui/material/ButtonBase' import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded' -import { cmn, cls, styles, TokenIcon, ChainIcon, type interfaces } from '@skalenetwork/metaport' import { DEFAULT_ERC20_DECIMALS } from '../core/constants' @@ -38,7 +39,7 @@ export default function TokenSurface(props: { className?: string tokenMetadata?: interfaces.TokenMetadata chainName?: string - skaleNetwork?: interfaces.SkaleNetwork + skaleNetwork?: types.SkaleNetwork }) { const [copied, setCopied] = useState(false) diff --git a/src/components/chains/HubApps.tsx b/src/components/chains/HubApps.tsx index 28d7d172..6406f4da 100644 --- a/src/components/chains/HubApps.tsx +++ b/src/components/chains/HubApps.tsx @@ -22,9 +22,10 @@ */ import { useState, ReactElement } from 'react' -import { Grid, Tooltip } from '@mui/material' +import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' -import { cmn, cls, type interfaces } from '@skalenetwork/metaport' +import { Grid, Tooltip } from '@mui/material' import { chainBg } from '../../core/metadata' import { sortObjectByKeys } from '../../core/helper' @@ -32,9 +33,9 @@ import { sortObjectByKeys } from '../../core/helper' import AppCard from '../AppCard' export default function HubApps(props: { - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork schainName: string - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap bg?: boolean all?: boolean }) { diff --git a/src/components/chains/HubTile.tsx b/src/components/chains/HubTile.tsx index 3626d778..3d04104d 100644 --- a/src/components/chains/HubTile.tsx +++ b/src/components/chains/HubTile.tsx @@ -25,7 +25,8 @@ import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { Tooltip } from '@mui/material' -import { cmn, cls, getChainAlias, type interfaces, SkPaper, styles } from '@skalenetwork/metaport' +import { cmn, cls, getChainAlias, SkPaper, styles } from '@skalenetwork/metaport' +import { type types } from '@/core' import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' @@ -34,20 +35,19 @@ import ChainLogo from '../ChainLogo' import { formatNumber } from '../../core/timeHelper' import { MAINNET_CHAIN_LOGOS } from '../../core/constants' -import { IChainMetrics, IMetrics } from '../../core/types' import { getChainDescription, getChainShortAlias } from '../../core/chain' import { chainBg } from '../../core/metadata' export default function HubTile(props: { - network: interfaces.SkaleNetwork - metrics: IMetrics | null + network: types.SkaleNetwork + metrics: types.IMetrics | null schainName: string isXs: boolean - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap bg?: boolean showStats?: boolean }) { - const [schainMetrics, setSchainMetrics] = useState(null) + const [schainMetrics, setSchainMetrics] = useState(null) useEffect(() => { if (props.metrics !== null && props.metrics.metrics[props.schainName]) { diff --git a/src/components/chains/tabs/ChainTabsSection.tsx b/src/components/chains/tabs/ChainTabsSection.tsx index 74c66580..effd9ca2 100644 --- a/src/components/chains/tabs/ChainTabsSection.tsx +++ b/src/components/chains/tabs/ChainTabsSection.tsx @@ -23,7 +23,8 @@ import { useEffect, useState } from 'react' -import { cmn, cls, type interfaces, MetaportCore, SkPaper } from '@skalenetwork/metaport' +import { cmn, cls, MetaportCore, SkPaper } from '@skalenetwork/metaport' +import { type types } from '@/core' import WidgetsRoundedIcon from '@mui/icons-material/WidgetsRounded' import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded' @@ -74,7 +75,7 @@ function CustomTabPanel(props: TabPanelProps) { export default function ChainTabsSection(props: { mpc: MetaportCore - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap schainName: string isXs: boolean }) { diff --git a/src/components/chains/tabs/DeveloperInfo.tsx b/src/components/chains/tabs/DeveloperInfo.tsx index 21b2662c..016c659e 100644 --- a/src/components/chains/tabs/DeveloperInfo.tsx +++ b/src/components/chains/tabs/DeveloperInfo.tsx @@ -21,7 +21,8 @@ * @copyright SKALE Labs 2024-Present */ -import { cmn, cls, styles, PROXY_ENDPOINTS, interfaces, SkPaper } from '@skalenetwork/metaport' +import { cmn, cls, styles, PROXY_ENDPOINTS, SkPaper } from '@skalenetwork/metaport' +import { type types } from '@/core' import Grid from '@mui/material/Grid' import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded' @@ -40,7 +41,7 @@ import Headline from '../../Headline' export default function DeveloperInfo(props: { schainName: string - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork className?: string }) { const proxyBase = PROXY_ENDPOINTS[props.skaleNetwork] diff --git a/src/components/chains/tabs/Tabs.tsx b/src/components/chains/tabs/Tabs.tsx index 721e2f81..edd6eb86 100644 --- a/src/components/chains/tabs/Tabs.tsx +++ b/src/components/chains/tabs/Tabs.tsx @@ -27,12 +27,13 @@ import { Link } from 'react-router-dom' import Tabs from '@mui/material/Tabs' import Tab from '@mui/material/Tab' -import { cls, cmn, interfaces } from '@skalenetwork/metaport' +import { cls, cmn } from '@skalenetwork/metaport' +import { type types } from '@/core' import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' export default function ChainTabs(props: { - chainMeta: interfaces.ChainMetadata + chainMeta: types.ChainMetadata handleChange: (event: React.SyntheticEvent, newValue: number) => void tabs: any[] tab: number @@ -43,11 +44,11 @@ export default function ChainTabs(props: {
{props.tabs.map((tab, index) => tab ? ( @@ -56,7 +57,7 @@ export default function ChainTabs(props: { label={tab.label} icon={tab.icon} iconPosition="start" - className={cls('btn', 'btnSm', cmn.mri10, cmn.mleft10, 'tab', 'fwmobile')} + className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} /> ) : null )} @@ -65,7 +66,7 @@ export default function ChainTabs(props: { label="Manage" icon={} iconPosition="start" - className={cls('btn', 'btnSm', cmn.mri10, cmn.mleft10, 'tab')} + className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab')} /> diff --git a/src/components/delegation/RetrieveRewardModal.tsx b/src/components/delegation/RetrieveRewardModal.tsx index bdcedf22..41e1bc5f 100644 --- a/src/components/delegation/RetrieveRewardModal.tsx +++ b/src/components/delegation/RetrieveRewardModal.tsx @@ -88,6 +88,7 @@ export default function RetrieveRewardModal(props: { onClose={handleClose} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" + className="skPopup" > . + */ + +/** + * @file AppCardV2.tsx + * @copyright SKALE Labs 2022-Present + */ + +import { Link } from 'react-router-dom' +import { cmn, cls, SkPaper, ChainIcon } from '@skalenetwork/metaport' +import { type types } from '@/core' + +import ChainLogo from '../ChainLogo' +import { MAINNET_CHAIN_LOGOS } from '../../core/constants' +import { getChainShortAlias } from '../../core/chain' +import { chainBg, getChainAlias } from '../../core/metadata' + +import CollapsibleDescription from '../CollapsibleDescription' +import AppCategoriesChips from './CategoriesShips' +import SocialButtons from './Socials' +import { Chip } from '@mui/material' + +export default function AppCard(props: { + skaleNetwork: types.SkaleNetwork + schainName: string + appName: string + chainsMeta: types.ChainsMetadataMap + transactions?: number +}) { + const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) + const url = `/chains/${shortAlias}/${props.appName}` + const appMeta = props.chainsMeta[props.schainName]?.apps?.[props.appName]! + + const appDescription = appMeta.description ?? 'No description' + + return ( + + +
+
+
+ +
+
+
+ +
+ +
+ +
+
+

+ {getChainAlias(props.chainsMeta, props.schainName, props.appName)} +

+ {appMeta.tags?.includes('pretge') && ( + + )} +
+ + + +
+
+ ) +} diff --git a/src/components/ecosystem/AppSearch.tsx b/src/components/ecosystem/AppSearch.tsx new file mode 100644 index 00000000..ee7d1549 --- /dev/null +++ b/src/components/ecosystem/AppSearch.tsx @@ -0,0 +1,64 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file AppSearch.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import TextField from '@mui/material/TextField' +import InputAdornment from '@mui/material/InputAdornment' +import SearchIcon from '@mui/icons-material/Search' +import { cmn, cls, styles } from '@skalenetwork/metaport' + +interface SearchComponentProps { + searchTerm: string + setSearchTerm: React.Dispatch> + className?: string +} + +const SearchComponent: React.FC = ({ + searchTerm, + setSearchTerm, + className +}) => { + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchTerm(event.target.value) + } + + return ( +
+ + + + ) + }} + className={cls('skInput')} + /> +
+ ) +} + +export default SearchComponent diff --git a/src/components/ecosystem/Categories.tsx b/src/components/ecosystem/Categories.tsx new file mode 100644 index 00000000..1c2f47ce --- /dev/null +++ b/src/components/ecosystem/Categories.tsx @@ -0,0 +1,268 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file Categories.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useState, useMemo, useRef, useEffect } from 'react' +import { cmn, cls } from '@skalenetwork/metaport' + +import { + type CheckedItems, + type ExpandedItems, + getSelectedSubcategoriesCount, + filterCategories +} from '../../core/ecosystem/categoryUtils' + +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' +import IconButton from '@mui/material/IconButton' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ExpandLessIcon from '@mui/icons-material/ExpandLess' +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' +import Menu from '@mui/material/Menu' +import Button from '@mui/material/Button' +import ManageSearchRoundedIcon from '@mui/icons-material/ManageSearchRounded' +import { categories, type Category } from '../../core/ecosystem/categories' + +import SearchBar, { highlightMatch } from './SearchBar' +import SubcategoryList from './SubcategoryList' + +interface CategoryDisplayProps { + checkedItems: CheckedItems + setCheckedItems: React.Dispatch> +} + +const CategoryDisplay: React.FC = ({ checkedItems, setCheckedItems }) => { + const [expandedItems, setExpandedItems] = useState({}) + const [anchorEl, setAnchorEl] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const [buttonWidth, setButtonWidth] = useState(undefined) + const buttonRef = useRef(null) + + useEffect(() => { + if (buttonRef.current != null) { + setButtonWidth(buttonRef.current.offsetWidth) + } + }, []) + + useEffect(() => { + Object.entries(categories).forEach(([category, data]) => { + const subs = data.subcategories + if (typeof subs === 'object' && !Array.isArray(subs)) { + const subcategoryKeys = Object.keys(subs) + const checkedSubcategories = subcategoryKeys.filter( + (sub) => checkedItems[`${category}_${sub}`] + ) + + if (checkedSubcategories.length > 0) { + setCheckedItems((prev) => { + const newState = { ...prev } + delete newState[category] + return newState + }) + } + } + }) + }, [checkedItems, setCheckedItems]) + + const handleCheck = (category: string, subcategory: string | null = null): void => { + setCheckedItems((prev) => { + const newState = { ...prev } + if (subcategory != null) { + const key = `${category}_${subcategory}` + newState[key] = !prev[key] + if (!newState[key]) delete newState[key] + delete newState[category] + } else { + setExpandedItems((prev) => ({ ...prev, [category]: true })) + if (newState[category]) { + delete newState[category] + } else { + newState[category] = true + const subs = categories[category].subcategories + if (typeof subs === 'object' && !Array.isArray(subs)) { + Object.keys(subs).forEach((subKey) => { + delete newState[`${category}_${subKey}`] + }) + } + } + } + return newState + }) + } + + const toggleExpand = (category: string): void => { + setExpandedItems((prev) => ({ + ...prev, + [category]: !prev[category] + })) + } + + const handleMenuOpen = (event: React.MouseEvent): void => { + setAnchorEl(event.currentTarget) + } + + const handleMenuClose = (): void => { + setAnchorEl(null) + } + + const handleSearch = (event: React.ChangeEvent): void => { + const newSearchTerm = event.target.value + setSearchTerm(newSearchTerm) + + const newExpandedItems = { ...expandedItems } + Object.entries(categories).forEach(([shortName, data]) => { + const subs = data.subcategories + if (typeof subs === 'object' && !Array.isArray(subs)) { + const hasMatchingSubcategory = Object.values(subs).some((sub) => + sub.name.toLowerCase().includes(newSearchTerm.toLowerCase()) + ) + const categoryMatches = data.name.toLowerCase().includes(newSearchTerm.toLowerCase()) + + newExpandedItems[shortName] = + hasMatchingSubcategory || (categoryMatches && newSearchTerm !== '') + } + }) + setExpandedItems(newExpandedItems) + } + + const handleClearSearch = (): void => { + setSearchTerm('') + setExpandedItems({}) + } + + const filteredCategories = useMemo(() => filterCategories(searchTerm), [searchTerm]) + + const renderSubcategories = (shortName: string, data: Category): JSX.Element | null => { + const subs = data.subcategories + if (typeof subs !== 'object' || Array.isArray(subs) || !expandedItems[shortName]) { + return null + } + + const filteredSubs = Object.entries(subs).reduce( + (acc, [key, value]) => { + if (value.name.toLowerCase().includes(searchTerm.toLowerCase()) || searchTerm === '') { + acc[key] = value + } + return acc + }, + {} as Record + ) + + if (Object.keys(filteredSubs).length === 0) { + return null + } + + return ( + + ) + } + + return ( +
+ + +
+ + {filteredCategories.map(([shortName, data], index) => ( +
+
+ { + handleCheck(shortName) + }} + /> + } + label={ + + {highlightMatch(data.name, searchTerm)} + + } + className={cls(cmn.flexg)} + /> + {getSelectedSubcategoriesCount(shortName, checkedItems) > 0 && ( + + )} + {typeof data.subcategories === 'object' && + !Array.isArray(data.subcategories) && + Object.keys(data.subcategories).length > 0 && ( + { + toggleExpand(shortName) + }} + size="small" + > + {expandedItems[shortName] ? : } + + )} +
+ {renderSubcategories(shortName, data)} +
+ ))} +
+
+
+ ) +} +export default CategoryDisplay diff --git a/src/components/ecosystem/CategoriesShips.tsx b/src/components/ecosystem/CategoriesShips.tsx new file mode 100644 index 00000000..0064dc97 --- /dev/null +++ b/src/components/ecosystem/CategoriesShips.tsx @@ -0,0 +1,90 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file CategoriesShips.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useMemo } from 'react' +import { type types } from '@/core' +import { Chip, Box } from '@mui/material' + +import { categories } from '../../core/ecosystem/categories' + +interface AppCategoriesChipsProps { + app: types.AppMetadata + className?: string +} + +const AppCategoriesChips: React.FC = ({ app, className }) => { + const chips = useMemo(() => { + if (!app.categories) return [] + + const getCategoryName = (tag: string) => categories[tag]?.name ?? tag + const getSubcategoryName = (categoryTag: string, subcategoryTag: string): string => { + const category = categories[categoryTag] + if (!category) return subcategoryTag + if (Array.isArray(category.subcategories)) { + return category.subcategories.includes(subcategoryTag) ? subcategoryTag : subcategoryTag + } + return category.subcategories[subcategoryTag]?.name ?? subcategoryTag + } + + return Object.entries(app.categories) + .flatMap(([categoryTag, subcategories]) => [ + , + ...(Array.isArray(subcategories) + ? subcategories.map((subTag) => ( + + )) + : []) + ]) + .slice(0, 3) // Limit to 3 chips + }, [app.categories]) + + if (chips.length === 0) return null + + return ( + *': { + flex: '0 0 auto' + }, + gap: 1 + }} + className={className} + > + {chips} + + ) +} + +export default AppCategoriesChips diff --git a/src/components/ecosystem/SearchBar.tsx b/src/components/ecosystem/SearchBar.tsx new file mode 100644 index 00000000..373bd6fd --- /dev/null +++ b/src/components/ecosystem/SearchBar.tsx @@ -0,0 +1,85 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file SearchBar.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import TextField from '@mui/material/TextField' +import InputAdornment from '@mui/material/InputAdornment' +import IconButton from '@mui/material/IconButton' +import SearchIcon from '@mui/icons-material/Search' +import ClearIcon from '@mui/icons-material/Clear' +import Tooltip from '@mui/material/Tooltip' +import { cmn, cls, styles } from '@skalenetwork/metaport' + +interface SearchBarProps { + searchTerm: string + onSearchChange: (event: React.ChangeEvent) => void + onClear: () => void + className?: string +} + +const SearchBar: React.FC = ({ + searchTerm, + onSearchChange, + onClear, + className +}) => ( +
+ + + + ), + endAdornment: ( + + + + + + ) + }} + /> +
+) + +export const highlightMatch = (text: string, searchTerm: string): React.ReactNode => { + if (!searchTerm) return text + const parts = text.split(new RegExp(`(${searchTerm})`, 'gi')) + return parts.map((part, index) => + part.toLowerCase() === searchTerm.toLowerCase() ? ( + + {part} + + ) : ( + part + ) + ) +} + +export default SearchBar diff --git a/src/components/ecosystem/SelectedCategories.tsx b/src/components/ecosystem/SelectedCategories.tsx new file mode 100644 index 00000000..5540e8ad --- /dev/null +++ b/src/components/ecosystem/SelectedCategories.tsx @@ -0,0 +1,136 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file SelectedCategories.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { cmn, cls, styles } from '@skalenetwork/metaport' + +import { Chip, Typography, Box } from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' +import { CheckedItems } from '../../core/ecosystem/categoryUtils' +import { categories, Category, Subcategory } from '../../core/ecosystem/categories' + +interface SelectedCategoriesProps { + checkedItems: CheckedItems + setCheckedItems: React.Dispatch> + filteredAppsCount: number +} + +const CustomChipLabel: React.FC<{ category: string; subcategory?: string }> = ({ + category, + subcategory +}) => ( + +

+ {category} +

+ {subcategory && ( + <> + +

{subcategory}

+ + )} +
+) + +const SelectedCategories: React.FC = ({ + checkedItems, + setCheckedItems, + filteredAppsCount +}) => { + const handleDelete = (category: string, fullSubcategory?: string) => { + setCheckedItems((prev) => { + const newCheckedItems = { ...prev } + if (fullSubcategory) { + delete newCheckedItems[fullSubcategory] + } else { + Object.keys(newCheckedItems).forEach((key) => { + if (key.startsWith(`${category}_`)) { + delete newCheckedItems[key] + } + }) + delete newCheckedItems[category] + } + return newCheckedItems + }) + } + + const clearAll = () => { + setCheckedItems({}) + } + + const getCategoryName = (key: string): string => categories[key]?.name || key + + const getSubcategoryName = (categoryKey: string, subcategoryKey: string): string => { + const category = categories[categoryKey] as Category + if (category && 'subcategories' in category) { + const subcategories = category.subcategories as { [key: string]: Subcategory } + return subcategories[subcategoryKey]?.name || subcategoryKey + } + return subcategoryKey + } + + const selectedItems = Object.entries(checkedItems).filter(([_, value]) => value) + + if (selectedItems.length === 0) return + + return ( + + {selectedItems.map(([key, _]) => { + const [category, subcategory] = key.split('_') + return ( + + } + onDelete={() => handleDelete(category, key)} + deleteIcon={} + className={cls(cmn.mri10, 'outlined', cmn.p600)} + /> + ) + })} + + {filteredAppsCount} project{filteredAppsCount !== 1 ? 's' : ''} + + + Clear all + + + ) +} + +export default SelectedCategories diff --git a/src/components/ecosystem/Socials.tsx b/src/components/ecosystem/Socials.tsx new file mode 100644 index 00000000..79723c55 --- /dev/null +++ b/src/components/ecosystem/Socials.tsx @@ -0,0 +1,161 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Socials.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { type types } from '@/core' + +import { IconButton, Tooltip } from '@mui/material' +import { LanguageRounded, FavoriteBorderOutlined, WavesRounded } from '@mui/icons-material' +import { cmn, cls } from '@skalenetwork/metaport' + +import { SocialIcon } from 'react-social-icons/component' +import 'react-social-icons/discord' +import 'react-social-icons/github' +import 'react-social-icons/telegram' +import 'react-social-icons/x' + +interface SocialButtonsProps { + social?: types.AppSocials + onAddToFavorites?: () => void + isFavorite?: boolean + className?: string +} + +const SocialButtons: React.FC = ({ + social, + onAddToFavorites, + isFavorite = false, + className +}) => { + if (!social) return undefined + return ( +
+
+ {social.website && ( + + + + + + )} + {social.x && ( + + + + + + )} + {social.telegram && ( + + + + + + )} + {social.discord && ( + + + + + + )} + {social.github && ( + + + + + + )} + {social.swell && ( + + + + + + )} +
+
+ + + + + +
+
+ ) +} + +export default SocialButtons diff --git a/src/components/ecosystem/SubcategoryList.tsx b/src/components/ecosystem/SubcategoryList.tsx new file mode 100644 index 00000000..6c909fe2 --- /dev/null +++ b/src/components/ecosystem/SubcategoryList.tsx @@ -0,0 +1,72 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file SubcategoryList.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' +import { type Subcategory } from '../../core/ecosystem/categories' +import { type CheckedItems } from '../../core/ecosystem/categoryUtils' +import { highlightMatch } from './SearchBar' +import { cmn, cls } from '@skalenetwork/metaport' + +interface SubcategoryListProps { + category: string + subcategories: Record + checkedItems: CheckedItems + onCheck: (category: string, subcategory: string) => void + searchTerm: string +} + +const SubcategoryList: React.FC = ({ + category, + subcategories, + checkedItems, + onCheck, + searchTerm +}) => ( +
+ {Object.entries(subcategories).map(([shortName, subcategory]) => ( +
+ { + onCheck(category, shortName) + }} + /> + } + label={ + + {highlightMatch(subcategory.name, searchTerm)} + + } + /> +
+ ))} +
+) + +export default SubcategoryList diff --git a/src/core/chain.ts b/src/core/chain.ts index 5cb21683..fe124c99 100644 --- a/src/core/chain.ts +++ b/src/core/chain.ts @@ -22,17 +22,16 @@ */ import { id, toBeHex } from 'ethers' -import { ISChain, ISChainData, TSChainArray } from './types' -import { interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' export const HTTPS_PREFIX = 'https://' export const WSS_PREFIX = 'wss://' -export function formatSChains(schainsData: ISChainData[]): ISChain[] { +export function formatSChains(schainsData: types.ISChainData[]): types.ISChain[] { return schainsData.map((schainData) => formatSChain(schainData.schain)) } -function formatSChain(schainArray: TSChainArray): ISChain { +function formatSChain(schainArray: types.TSChainArray): types.ISChain { return { name: schainArray[0], mainnetOwner: schainArray[1], @@ -66,15 +65,15 @@ export function getChainId(schainName: string): string { return toBeHex(id(schainName).substring(0, 15)) } -export function getChainShortAlias(meta: interfaces.ChainsMetadataMap, name: string): string { +export function getChainShortAlias(meta: types.ChainsMetadataMap, name: string): string { return meta[name]?.shortAlias !== undefined ? meta[name].shortAlias! : name } -export function getChainDescription(meta: interfaces.ChainMetadata | undefined): string { +export function getChainDescription(meta: types.ChainMetadata | undefined): string { return meta && meta.description ? meta.description : 'No description' } -export function findChainName(meta: interfaces.ChainsMetadataMap, name: string): string { +export function findChainName(meta: types.ChainsMetadataMap, name: string): string { for (const key in meta) { if (meta[key].shortAlias === name) { return key diff --git a/src/core/constants.ts b/src/core/constants.ts index 14cebfca..ff4c5d52 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -21,13 +21,13 @@ * @copyright SKALE Labs 2022-Present */ +import { type types } from '@/core' import FAQ from '../data/faq.json' import * as MAINNET_CHAIN_LOGOS from '../meta/logos' import * as VALIDATOR_LOGOS from '../assets/validators' import { CONTRACTS_META } from '../data/contractsMeta.ts' -import { interfaces } from '@skalenetwork/metaport' export const MAINNET_CHAIN_NAME = 'mainnet' @@ -72,7 +72,7 @@ export const TRANSAK_API_KEY = import.meta.env.VITE_TRANSAK_API_KEY export const DAPP_RADAR_BASE_URL = 'https://dappradar.com/dapp/' -export const STATS_API: { [key in interfaces.SkaleNetwork]: string | null } = { +export const STATS_API: { [key in types.SkaleNetwork]: string | null } = { mainnet: 'https://stats.explorer.mainnet.skalenodes.com/v2/stats/', testnet: null, staging: null, @@ -81,4 +81,4 @@ export const STATS_API: { [key in interfaces.SkaleNetwork]: string | null } = { } export const BASE_METADATA_URL = - 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' + 'https://raw.githubusercontent.com/skalenetwork/skale-network/metadata-v2/metadata/' // todo: tmp diff --git a/src/core/contracts.ts b/src/core/contracts.ts index c4b26461..e59829bb 100644 --- a/src/core/contracts.ts +++ b/src/core/contracts.ts @@ -24,6 +24,7 @@ import debug from 'debug' import { type Contract, type Signer } from 'ethers' import { skaleContracts, type Instance } from '@skalenetwork/skale-contracts-ethers-v6' import { type MetaportCore, type interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' import { initSkaleToken } from './delegation' import { type ContractType, DelegationType, type ISkaleContractsMap } from './interfaces' @@ -58,7 +59,7 @@ export async function initActionContract( signer: Signer, delegationType: DelegationType, beneficiary: interfaces.AddressType, - skaleNetwork: interfaces.SkaleNetwork, + skaleNetwork: types.SkaleNetwork, contractType: ContractType ): Promise { log('initActionContract:', skaleNetwork, beneficiary, contractType, delegationType) @@ -76,7 +77,7 @@ export async function initActionContract( return connectedContract(contract, signer) } -function getInstanceTag(skaleNetwork: interfaces.SkaleNetwork, projectName: PROJECT_TYPE): string { +function getInstanceTag(skaleNetwork: types.SkaleNetwork, projectName: PROJECT_TYPE): string { if (CONTRACTS_META[skaleNetwork].auto) { if (projectName === 'grants') return 'grants' return 'production' @@ -90,7 +91,7 @@ function connectedContract(contract: Contract, signer: Signer): Contract { async function getEscrowContract( network: any, - skaleNetwork: interfaces.SkaleNetwork, + skaleNetwork: types.SkaleNetwork, delegationType: DelegationType, beneficiary: interfaces.AddressType ): Promise { @@ -105,7 +106,7 @@ async function getEscrowContract( async function getManagerContract( network: any, - skaleNetwork: interfaces.SkaleNetwork, + skaleNetwork: types.SkaleNetwork, name: string ): Promise { const managerProject = await network.getProject('skale-manager') @@ -115,7 +116,7 @@ async function getManagerContract( async function getInstance( project: any, - skaleNetwork: interfaces.SkaleNetwork, + skaleNetwork: types.SkaleNetwork, tag: PROJECT_TYPE ): Promise { return project.getInstance(getInstanceTag(skaleNetwork, tag)) diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts new file mode 100644 index 00000000..6fe37a94 --- /dev/null +++ b/src/core/ecosystem/apps.ts @@ -0,0 +1,89 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file apps.ts + * @copyright SKALE Labs 2024-Present + */ + +import { type types } from '@/core' + +export interface AppWithChainAndName extends types.AppMetadata { + chain: string + appName: string +} + +interface CategoryFilter { + [key: string]: boolean +} + +export function getAllApps(chainsMetadata: types.ChainsMetadataMap): AppWithChainAndName[] { + const allApps: AppWithChainAndName[] = [] + + for (const [chainName, chainData] of Object.entries(chainsMetadata)) { + if (chainData.apps) { + for (const [appName, appData] of Object.entries(chainData.apps)) { + allApps.push({ + ...appData, + chain: chainName, + appName + }) + } + } + } + + return allApps +} + +export function sortAppsByAlias(apps: AppWithChainAndName[]): AppWithChainAndName[] { + return apps.sort((a, b) => a.alias.localeCompare(b.alias)) +} + +export function filterAppsByCategory( + apps: AppWithChainAndName[], + filter: CategoryFilter +): AppWithChainAndName[] { + if (Object.keys(filter).length === 0) return apps + return apps.filter((app) => { + if (!app.categories || Object.keys(app.categories).length === 0) return false + return Object.entries(app.categories).some(([category, subcategories]) => { + // Check if the main category is selected in the filter + if (filter[category] === true) return true + // If the main category isn't selected, check subcategories + if (Array.isArray(subcategories)) { + return subcategories.some((subcategory) => { + const subcategoryKey = `${category}_${subcategory}` + return filter[subcategoryKey] === true + }) + } + + return false + }) + }) +} + +export function filterAppsBySearchTerm( + apps: AppWithChainAndName[], + searchTerm: string +): AppWithChainAndName[] { + if (!searchTerm || searchTerm === '') return apps + return apps.filter( + (app) => + app.alias.toLowerCase().includes(searchTerm.toLowerCase()) || + app.chain.toLowerCase().includes(searchTerm.toLowerCase()) + ) +} diff --git a/src/core/ecosystem/categories.ts b/src/core/ecosystem/categories.ts new file mode 100644 index 00000000..6c822b6a --- /dev/null +++ b/src/core/ecosystem/categories.ts @@ -0,0 +1,95 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file categories.tsx + * @copyright SKALE Labs 2024-Present + */ + +export interface Subcategory { + name: string +} + +export interface Category { + name: string + subcategories: { [key: string]: Subcategory } | string[] +} + +export interface Categories { + [key: string]: Category +} + +export const categories: Categories = { + 'hub-chains': { name: 'Hub Chains', subcategories: {} }, + ai: { name: 'AI', subcategories: {} }, + 'bridges-onramps': { name: 'Bridges + On-ramps', subcategories: {} }, + cex: { name: 'CEX', subcategories: ['Add Later'] }, + consumer: { name: 'Consumer', subcategories: ['Add Later'] }, + defi: { + name: 'DeFi', + subcategories: { + aggregators: { name: 'Aggregators' }, + betting: { name: 'Betting' }, + custody: { name: 'Custody' }, + dex: { name: 'DEX' }, + launchpads: { name: 'Launchpads' }, + lending: { name: 'Lending' }, + lottery: { name: 'Lottery' }, + options: { name: 'Options' }, + payments: { name: 'Payments' }, + 'perps-derivatives': { name: 'Perps + Derivatives' }, + 'prediction-markets': { name: 'Prediction Markets' }, + stablecoins: { name: 'Stablecoins' }, + synthetics: { name: 'Synthetics' }, + yield: { name: 'Yield' } + } + }, + gaming: { + name: 'Gaming', + subcategories: { + 'action-adventure': { name: 'Action/Adventure' }, + 'battle-royale': { name: 'Battle Royale' }, + casual: { name: 'Casual' }, + 'cards_deck-building': { name: 'Cards + Deck Building' }, + console: { name: 'Console' }, + fighting: { name: 'Fighting' }, + shooter: { name: 'Shooter' }, + metaverse: { name: 'Metaverse' }, + mmorpg: { name: 'MMORPG' }, + mobile: { name: 'Mobile' }, + pc: { name: 'PC' }, + browser: { name: 'Browser' }, + platformer: { name: 'Platformer' }, + puzzle: { name: 'Puzzle' }, + racing: { name: 'Racing' }, + rpg: { name: 'RPG' }, + sandbox: { name: 'Sandbox' }, + simulation: { name: 'Simulation' }, + sports: { name: 'Sports' }, + strategy: { name: 'Strategy' } + } + }, + infrastructure: { name: 'Infrastructure', subcategories: {} }, + mobile: { name: 'Mobile', subcategories: {} }, + oracles: { name: 'Oracles', subcategories: {} }, + wallets: { name: 'Wallets', subcategories: {} }, + analytics: { name: 'Analytics', subcategories: {} }, + daos: { name: 'DAOs', subcategories: {} }, + socialfi: { name: 'SocialFi', subcategories: {} }, + nfts: { name: 'NFTs', subcategories: {} } +} diff --git a/src/core/ecosystem/categoryUtils.ts b/src/core/ecosystem/categoryUtils.ts new file mode 100644 index 00000000..87a7a1e0 --- /dev/null +++ b/src/core/ecosystem/categoryUtils.ts @@ -0,0 +1,65 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file categoryUtils.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { categories } from './categories' + +export interface CheckedItems { + [key: string]: boolean +} + +export interface ExpandedItems { + [key: string]: boolean +} + +export const getSelectedSubcategoriesCount = ( + category: string, + checkedItems: CheckedItems +): number => { + const subcategories = categories[category].subcategories + if (typeof subcategories === 'object' && !Array.isArray(subcategories)) { + return Object.keys(subcategories).filter((subKey) => checkedItems[`${category}_${subKey}`]) + .length + } + return 0 +} + +export const getSelectedCategoriesCount = (checkedItems: CheckedItems): number => { + return Object.keys(categories).filter( + (category) => + checkedItems[category] || + Object.keys(checkedItems).some((key) => key.startsWith(`${category}_`)) + ).length +} + +export const filterCategories = (searchTerm: string) => { + return Object.entries(categories).filter(([_, data]) => { + const categoryMatch = data.name.toLowerCase().includes(searchTerm.toLowerCase()) + const subcategoryMatch = + typeof data.subcategories === 'object' && + !Array.isArray(data.subcategories) && + Object.values(data.subcategories).some((sub) => + sub.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + return categoryMatch || subcategoryMatch + }) +} diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 04758b2b..3acb773c 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -22,20 +22,22 @@ import { BASE_EXPLORER_URLS, interfaces } from '@skalenetwork/metaport' import { HTTPS_PREFIX } from './chain' -import { IAddressCounters, IAppCounters, IMetricsChainMap, IAppId } from './types' +import { type types } from '@/core' export function addressUrl(explorerUrl: string, address: string): string { return `${explorerUrl}/address/${address}` } -export function getExplorerUrl(network: interfaces.SkaleNetwork, chainName: string): string { +export function getExplorerUrl(network: types.SkaleNetwork, chainName: string): string { const explorerBaseUrl = BASE_EXPLORER_URLS[network] return HTTPS_PREFIX + chainName + '.' + explorerBaseUrl } -export function getTotalAppCounters(countersArray: IAppCounters | null): IAddressCounters | null { +export function getTotalAppCounters( + countersArray: types.IAppCounters | null +): types.IAddressCounters | null { if (countersArray === null) return null - const totalCounters: IAddressCounters = { + const totalCounters: types.IAddressCounters = { gas_usage_count: '0', token_transfers_count: '0', transactions_count: '0', @@ -63,7 +65,10 @@ export function getTotalAppCounters(countersArray: IAppCounters | null): IAddres return totalCounters } -export function getTopAppsByTransactions(metrics: IMetricsChainMap, topN: number): Array { +export function getTopAppsByTransactions( + metrics: types.IMetricsChainMap, + topN: number +): Array { let appsWithCounters = [] for (let chain in metrics) { for (let app in metrics[chain].apps_counters) { diff --git a/src/core/metadata.ts b/src/core/metadata.ts index 9c0e6a8f..65a5c237 100644 --- a/src/core/metadata.ts +++ b/src/core/metadata.ts @@ -20,32 +20,27 @@ * @copyright SKALE Labs 2024-Present */ -import { type interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' import { BASE_METADATA_URL, MAINNET_CHAIN_NAME } from './constants' export function chainBg( - chainsMeta: interfaces.ChainsMetadataMap, + chainsMeta: types.ChainsMetadataMap, chainName: string, app?: string -): string { +): string | undefined { const chainData = chainsMeta[chainName] if (chainData) { const appData = chainData.apps && app ? chainData.apps[app] : null - return ( - appData?.gradientBackground || - appData?.background || - chainData.gradientBackground || - chainData.background - ) + return appData?.gradientBackground || chainData.gradientBackground || chainData.background } return 'linear-gradient(273.67deg, rgb(47 50 80), rgb(39 43 68))' } export function getChainAlias( - chainsMeta: interfaces.ChainsMetadataMap, + chainsMeta: types.ChainsMetadataMap, chainName: string, app?: string ): string { @@ -66,13 +61,11 @@ function transformChainName(chainName: string): string { .join(' ') } -export async function loadMeta( - skaleNetwork: interfaces.SkaleNetwork -): Promise { +export async function loadMeta(skaleNetwork: types.SkaleNetwork): Promise { const response = await fetch(`${BASE_METADATA_URL}${skaleNetwork}/chains.json`) return await response.json() } -export function getMetaLogoUrl(skaleNetwork: interfaces.SkaleNetwork, logoName: string): string { +export function getMetaLogoUrl(skaleNetwork: types.SkaleNetwork, logoName: string): string { return `${BASE_METADATA_URL}${skaleNetwork}/logos/${logoName}` } diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts index e44fb5f3..678535e0 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -21,7 +21,8 @@ */ import { Contract, id, type InterfaceAbi } from 'ethers' -import { type MetaportCore, type interfaces } from '@skalenetwork/metaport' +import { type MetaportCore } from '@skalenetwork/metaport' +import { type types } from '@/core' import PAYMASTER_INFO from '../data/paymaster' export interface PaymasterInfo { @@ -53,15 +54,15 @@ export function divideBigInts(a: bigint, b: bigint): number { return Number((a * 10000n) / b) / 10000 } -export function getPaymasterChain(skaleNetwork: interfaces.SkaleNetwork): string { +export function getPaymasterChain(skaleNetwork: types.SkaleNetwork): string { return PAYMASTER_INFO.networks[skaleNetwork].chain } -export function getPaymasterAddress(skaleNetwork: interfaces.SkaleNetwork): string { +export function getPaymasterAddress(skaleNetwork: types.SkaleNetwork): string { return PAYMASTER_INFO.networks[skaleNetwork].address } -export function getPaymasterLaunchTs(skaleNetwork: interfaces.SkaleNetwork): bigint { +export function getPaymasterLaunchTs(skaleNetwork: types.SkaleNetwork): bigint { return BigInt(PAYMASTER_INFO.networks[skaleNetwork].launchTs) } @@ -80,7 +81,7 @@ export function initPaymaster(mpc: MetaportCore): Contract { export async function getPaymasterInfo( paymaster: Contract, targetChainName: string, - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork ): Promise { const rawData = await Promise.all([ paymaster.maxReplenishmentPeriod(), diff --git a/src/core/transferHistory.ts b/src/core/transferHistory.ts index be305d43..f6d02afe 100644 --- a/src/core/transferHistory.ts +++ b/src/core/transferHistory.ts @@ -21,13 +21,14 @@ */ import { type interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' -function getKeyName(skaleNetwork: interfaces.SkaleNetwork): string { +function getKeyName(skaleNetwork: types.SkaleNetwork): string { return `br__transfersHistory_${skaleNetwork}` } export function getHistoryFromStorage( - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork ): interfaces.TransferHistory[] { const transfersHistory = localStorage.getItem(getKeyName(skaleNetwork)) if (transfersHistory == null) return [] @@ -36,11 +37,11 @@ export function getHistoryFromStorage( export function setHistoryToStorage( transferHistory: interfaces.TransferHistory[], - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork ): void { localStorage.setItem(getKeyName(skaleNetwork), JSON.stringify({ data: transferHistory })) } -export function clearTransferHistory(skaleNetwork: interfaces.SkaleNetwork): void { +export function clearTransferHistory(skaleNetwork: types.SkaleNetwork): void { localStorage.removeItem(getKeyName(skaleNetwork)) } diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 370f2700..00000000 --- a/src/core/types.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/** - * @file types.ts - * @copyright SKALE Labs 2024-Present - */ - -import { interfaces } from '@skalenetwork/metaport' -import { ReactElement } from 'react' - -export type TSChainArray = [ - string, - interfaces.AddressType, - number, - number, - number, - number, - number, - number, - number, - number, - interfaces.AddressType, - boolean, - boolean -] - -export interface ISChain { - name: string - mainnetOwner: interfaces.AddressType - indexInOwnerList: number - partOfNode: number - lifetime: number - startDate: number - startBlock: number - deposit: number - index: number - generation: number - originator: interfaces.AddressType - multitransactionMode: boolean - thresholdEncryption: boolean -} - -export interface ISChainData { - schain: TSChainArray - nodes: INodeInfo[] -} - -export interface INodeInfo { - id: number - name: string - ip: string - base_port: number - domain: string - schain_base_port: number - httpRpcPort: number - httpsRpcPort: number - wsRpcPort: number - wssRpcPort: number - infoHttpRpcPort: number - http_endpoint_ip: string - https_endpoint_ip: string - ws_endpoint_ip: string - wss_endpoint_ip: string - infoHttp_endpoint_ip: string - http_endpoint_domain: string - https_endpoint_domain: string - ws_endpoint_domain: string - wss_endpoint_domain: string - infoHttp_endpoint_domain: string - block_ts: number -} - -export interface IGasInfo { - LastBlock: string - SafeGasPrice: string - ProposeGasPrice: string - FastGasPrice: string - suggestBaseFee: string - gasUsedRatio: string -} - -export interface IMetrics { - gas: number - last_updated: number - metrics: IMetricsChainMap -} - -export interface IMetricsChainMap { - [chainName: string]: IChainMetrics -} - -export interface IChainMetrics { - chain_stats: any - apps_counters: IAppCountersMap -} - -export interface IAppCountersMap { - [appName: string]: IAppCounters | null -} - -export interface IAppCounters { - [contractAddress: interfaces.AddressType]: IAddressCounters -} - -export interface IAddressCounters { - gas_usage_count: string - token_transfers_count: string - transactions_count: string - validations_count: string -} - -export interface IStats { - schains_number: number - summary: IStatsMap - schains: { [schainName: string]: IStatsMap } -} - -export interface IStatsMap { - total: IStatsData - total_7d: IStatsData - total_30d: IStatsData - group_by_month: any -} - -export interface IStatsData { - tx_count_total: number - block_count_total: number - gas_total_used: number - gas_fees_total_gwei: number - gas_fees_total_eth: number - gas_fees_total_usd: number - users_count_total: number -} - -export interface IAppId { - app: string - chain: string - totalTransactions?: number -} - -export interface BreadcrumbSection { - icon: ReactElement - text: string - url?: string -} diff --git a/src/data/contractsMeta.ts b/src/data/contractsMeta.ts index debc9ab0..57ed669c 100644 --- a/src/data/contractsMeta.ts +++ b/src/data/contractsMeta.ts @@ -21,9 +21,9 @@ * @copyright SKALE Labs 2022-Present */ -import { type interfaces } from '@skalenetwork/metaport' +import { type types } from '@/core' -export const CONTRACTS_META: { [key in interfaces.SkaleNetwork]: any } = { +export const CONTRACTS_META: { [key in types.SkaleNetwork]: any } = { mainnet: { auto: true }, diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 450ce94d..808b1a2a 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -25,15 +25,8 @@ import { useEffect, useState } from 'react' import { Helmet } from 'react-helmet' import { useParams } from 'react-router-dom' -import { - MetaportCore, - fromWei, - interfaces, - styles, - cmn, - cls, - SkPaper -} from '@skalenetwork/metaport' +import { MetaportCore, fromWei, styles, cmn, cls, SkPaper } from '@skalenetwork/metaport' +import { type types } from '@/core' import { Button, Grid } from '@mui/material' import Container from '@mui/material/Container' @@ -60,7 +53,6 @@ import AccordionSection from '../components/AccordionSection' import { findChainName } from '../core/chain' -import { IAddressCounters, IMetrics } from '../core/types' import { formatNumber } from '../core/timeHelper' import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' @@ -69,16 +61,16 @@ import { DAPP_RADAR_BASE_URL, MAINNET_CHAIN_LOGOS } from '../core/constants' export default function App(props: { mpc: MetaportCore loadData: () => Promise - metrics: IMetrics | null + metrics: types.IMetrics | null isXs: boolean - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap }) { let { chain, app } = useParams() if (chain === undefined || app === undefined) return 'No such app' const network = props.mpc.config.skaleNetwork const [expanded, setExpanded] = useState('panel3') - const [counters, setCounters] = useState(null) + const [counters, setCounters] = useState(null) chain = findChainName(props.chainsMeta, chain ?? '') @@ -167,7 +159,7 @@ export default function App(props: { children={

{appAlias}

- +
} /> @@ -178,8 +170,13 @@ export default function App(props: { grow={!appMeta.contracts} children={
- {appMeta.url ? ( - + {appMeta.social?.website ? ( +
- + ) } diff --git a/src/pages/Chain.tsx b/src/pages/Chain.tsx index d504a2ff..f0ed90ad 100644 --- a/src/pages/Chain.tsx +++ b/src/pages/Chain.tsx @@ -28,21 +28,22 @@ import Container from '@mui/material/Container' import SchainDetails from '../components/SchainDetails' import CircularProgress from '@mui/material/CircularProgress' -import { cmn, cls, type MetaportCore, type interfaces } from '@skalenetwork/metaport' -import { IChainMetrics, IMetrics, ISChain, IStats, IStatsData } from '../core/types' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' +import { type types } from '@/core' + import { findChainName } from '../core/chain' export default function Chain(props: { loadData: any - schains: ISChain[] - stats: IStats | null - metrics: IMetrics | null + schains: types.ISChain[] + stats: types.IStats | null + metrics: types.IMetrics | null mpc: MetaportCore - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap isXs: boolean }) { - const [schainStats, setSchainStats] = useState(null) - const [schainMetrics, setSchainMetrics] = useState(null) + const [schainStats, setSchainStats] = useState(null) + const [schainMetrics, setSchainMetrics] = useState(null) let { name } = useParams() const chainName: string = findChainName(props.chainsMeta, name ?? '') diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index 3856de6f..418975e1 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -24,6 +24,10 @@ import { Helmet } from 'react-helmet' import { useState, useEffect } from 'react' + +import { cmn, cls, styles, type MetaportCore } from '@skalenetwork/metaport' +import { type types } from '@/core' + import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import CircularProgress from '@mui/material/CircularProgress' @@ -33,20 +37,17 @@ import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded' import HubsSection from '../components/HubsSection' import { getPrimaryCategory } from '../components/CategoryBadge' -import { cmn, cls, styles, type MetaportCore, type interfaces } from '@skalenetwork/metaport' - import { META_TAGS } from '../core/meta' import { Button } from '@mui/material' import AppChains from '../components/AppChains' -import { IMetrics, ISChain } from '../core/types' export default function Chains(props: { loadData: () => Promise - schains: ISChain[] - metrics: IMetrics | null + schains: types.ISChain[] + metrics: types.IMetrics | null mpc: MetaportCore isXs: boolean - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap }) { const [_, setIntervalId] = useState() diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx new file mode 100644 index 00000000..da053770 --- /dev/null +++ b/src/pages/Ecosystem.tsx @@ -0,0 +1,190 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Ecosystem.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useEffect, useState } from 'react' +import { Helmet } from 'react-helmet' + +import Container from '@mui/material/Container' +import Stack from '@mui/material/Stack' +import Box from '@mui/material/Box' +import Grid from '@mui/material/Grid' + +import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded' +import EditRoundedIcon from '@mui/icons-material/EditRounded' +import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' +import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded' +import StarRoundedIcon from '@mui/icons-material/StarRounded' + +import { type types } from '@/core' + +import AppCard from '../components/ecosystem/AppCardV2' + +import { cmn, cls, type MetaportCore, styles } from '@skalenetwork/metaport' +import { META_TAGS } from '../core/meta' +import { Button, Tab, Tabs } from '@mui/material' +import CategoryDisplay from '../components/ecosystem/Categories' +import { + AppWithChainAndName, + filterAppsByCategory, + filterAppsBySearchTerm, + getAllApps, + sortAppsByAlias +} from '../core/ecosystem/apps' +import { CheckedItems } from '../core/ecosystem/categoryUtils' +import SearchComponent from '../components/ecosystem/AppSearch' +import SelectedCategories from '../components/ecosystem/SelectedCategories' +import SkStack from '../components/SkStack' + +export default function Ecosystem(props: { + mpc: MetaportCore + chainsMeta: types.ChainsMetadataMap + isXs: boolean +}) { + const allApps = sortAppsByAlias(getAllApps(props.chainsMeta)) + const [checkedItems, setCheckedItems] = useState({}) + const [apps, setApps] = useState([]) + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => { + setApps(allApps) + }, [props.chainsMeta]) + + useEffect(() => { + setApps(filterAppsBySearchTerm(filterAppsByCategory(allApps, checkedItems), searchTerm)) + }, [checkedItems, searchTerm]) + + console.log('render Ecosystem') + console.log(checkedItems) + + return ( + + + {META_TAGS.apps.title} + + + + + +
+

Ecosystem

+
+

+ Explore dApps across the SKALE ecosystem +

+ + + + + + + {}} + scrollButtons="auto" + className={cls( + cmn.mbott20, + [cmn.mtop20, Object.keys(checkedItems).length !== 0], + 'skTabs' + )} + > + } + iconPosition="start" + className={cls('btn', 'btnSm', cmn.mri5, 'tab', 'fwmobile')} + /> + } + iconPosition="start" + className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} + /> + } + iconPosition="start" + className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} + /> + } + iconPosition="start" + className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} + /> + + + + {apps.map((app) => ( + + + + ))} + +
+
+ + + ) +} diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index fda7e6f7..b274b2af 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -21,6 +21,9 @@ * @copyright SKALE Labs 2023-Present */ +import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' + import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' @@ -28,23 +31,25 @@ import Grid from '@mui/material/Grid' import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' -import InsertChartOutlinedIcon from '@mui/icons-material/InsertChartOutlined' -import WalletOutlinedIcon from '@mui/icons-material/WalletOutlined' + +import LabelImportantRoundedIcon from '@mui/icons-material/LabelImportantRounded' +import RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded' +import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' +import PieChartOutlineRoundedIcon from '@mui/icons-material/PieChartOutlineRounded' import PageCard from '../components/PageCard' +import AppCard from '../components/ecosystem/AppCardV2' -import { cmn, cls, interfaces } from '@skalenetwork/metaport' -import AppCard from '../components/AppCard' -import { IAppId } from '../core/types' import { useEffect, useState } from 'react' import FeaturedApps from '../components/FeaturedApps' export default function Start(props: { isXs: boolean - skaleNetwork: interfaces.SkaleNetwork - topApps: IAppId[] | null + skaleNetwork: types.SkaleNetwork + topApps: types.IAppId[] | null loadData: () => Promise - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap }) { const [_, setIntervalId] = useState() @@ -67,8 +72,8 @@ export default function Start(props: { : null if (apps) { - appCards = apps.slice(0, 4).map((topApp: IAppId) => ( - + appCards = apps.slice(0, 3).map((topApp: types.IAppId) => ( + -

🔥 Top Apps on SKALE

- - {appCards} - -

- ⭐ Featured Apps -

- -

- 🪐 Explore Portal -

+

Welcome to SKALE

+
+ +

Explore Portal

+
+ @@ -105,27 +105,42 @@ export default function Start(props: { } + description="Manage delegations and validators" + name="stake" + icon={} /> } + description="Apps, games, block explorers and endpoints" + name="chains" + icon={} /> } + description="Discover apps and games on SKALE" + name="ecosystem" + icon={} /> + +
+ +

New dApps on SKALE

+
+ + +
+ +

Trending dApps on SKALE

+
+ + + {appCards} +
) diff --git a/src/styles/components.scss b/src/styles/components.scss new file mode 100644 index 00000000..2de51fb5 --- /dev/null +++ b/src/styles/components.scss @@ -0,0 +1,96 @@ +.skTabs { + .MuiTab-root { + background-color: $sk-bg !important; + } +} + + + + +.skAppCard { + padding: 16px; + + .logo-container { + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; + } + + .logo-wrapper { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: 15px; + } + + .responsive-logo { + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + padding: 10px + } + + + .MuiChip-label { + font-weight: 600; + font-size: 0.7rem; + } + +} + +.skInput { + .MuiInputBase-root { + min-height: 53px; + border-radius: $sk-border-radius; + border: 1px $border-color solid; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif !important; + } + + .MuiOutlinedInput-input { + padding: 14px 0 !important; + } + + .MuiOutlinedInput-notchedOutline { + display: none; + } + + input { + font-size: 0.8025rem !important; + line-height: 1.6 !important; + letter-spacing: 0.02857em !important; + font-weight: 600 !important; + } +} + +.outlined { + border-radius: $sk-border-radius; + border: 1px $border-color solid; +} + +.borderLeft { + border-left: 1px $border-color solid; +} + +.skMenuBtn { + min-height: 53px; +} + +.skMenu { + .MuiMenu-paper { + background-image: none !important; + border-radius: $sk-border-radius; + border: 1px $border-color solid; + } + + .MuiCheckbox-root { + padding: 2px 10px; + } +} \ No newline at end of file From c2dd97494df252b0ddfb409fa53fd067ddd92476 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 7 Aug 2024 18:40:20 +0100 Subject: [PATCH 05/39] Update filter categories, fix text on Start and Ecosystem pages --- src/components/ecosystem/Categories.tsx | 88 +++++++------------ .../ecosystem/SelectedCategories.tsx | 37 +++----- src/components/ecosystem/SubcategoryList.tsx | 15 ++-- src/core/ecosystem/apps.ts | 15 ++-- src/core/ecosystem/categoryUtils.ts | 27 +++--- src/pages/Chains.tsx | 47 +--------- src/pages/Ecosystem.tsx | 11 ++- src/pages/Start.tsx | 2 +- 8 files changed, 73 insertions(+), 169 deletions(-) diff --git a/src/components/ecosystem/Categories.tsx b/src/components/ecosystem/Categories.tsx index 1c2f47ce..601c1ac7 100644 --- a/src/components/ecosystem/Categories.tsx +++ b/src/components/ecosystem/Categories.tsx @@ -23,12 +23,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react' import { cmn, cls } from '@skalenetwork/metaport' -import { - type CheckedItems, - type ExpandedItems, - getSelectedSubcategoriesCount, - filterCategories -} from '../../core/ecosystem/categoryUtils' +import { filterCategories } from '../../core/ecosystem/categoryUtils' import Checkbox from '@mui/material/Checkbox' import FormControlLabel from '@mui/material/FormControlLabel' @@ -45,12 +40,12 @@ import SearchBar, { highlightMatch } from './SearchBar' import SubcategoryList from './SubcategoryList' interface CategoryDisplayProps { - checkedItems: CheckedItems - setCheckedItems: React.Dispatch> + checkedItems: string[] + setCheckedItems: React.Dispatch> } const CategoryDisplay: React.FC = ({ checkedItems, setCheckedItems }) => { - const [expandedItems, setExpandedItems] = useState({}) + const [expandedItems, setExpandedItems] = useState>({}) const [anchorEl, setAnchorEl] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [buttonWidth, setButtonWidth] = useState(undefined) @@ -62,50 +57,31 @@ const CategoryDisplay: React.FC = ({ checkedItems, setChec } }, []) - useEffect(() => { - Object.entries(categories).forEach(([category, data]) => { - const subs = data.subcategories - if (typeof subs === 'object' && !Array.isArray(subs)) { - const subcategoryKeys = Object.keys(subs) - const checkedSubcategories = subcategoryKeys.filter( - (sub) => checkedItems[`${category}_${sub}`] - ) - - if (checkedSubcategories.length > 0) { - setCheckedItems((prev) => { - const newState = { ...prev } - delete newState[category] - return newState - }) - } - } - }) - }, [checkedItems, setCheckedItems]) - const handleCheck = (category: string, subcategory: string | null = null): void => { setCheckedItems((prev) => { - const newState = { ...prev } - if (subcategory != null) { - const key = `${category}_${subcategory}` - newState[key] = !prev[key] - if (!newState[key]) delete newState[key] - delete newState[category] + let newCheckedItems = [...prev] + const itemToToggle = subcategory ? `${category}_${subcategory}` : category + + if (newCheckedItems.includes(itemToToggle)) { + return newCheckedItems.filter((item) => item !== itemToToggle) } else { - setExpandedItems((prev) => ({ ...prev, [category]: true })) - if (newState[category]) { - delete newState[category] - } else { - newState[category] = true - const subs = categories[category].subcategories - if (typeof subs === 'object' && !Array.isArray(subs)) { - Object.keys(subs).forEach((subKey) => { - delete newState[`${category}_${subKey}`] - }) + if (subcategory) { + // Remove the main category if it was checked + const mainCategoryIndex = newCheckedItems.indexOf(category) + if (mainCategoryIndex !== -1) { + newCheckedItems.splice(mainCategoryIndex, 1) } + } else { + // Remove all subcategories of this category + newCheckedItems = newCheckedItems.filter((item) => !item.startsWith(`${category}_`)) } + return [...newCheckedItems, itemToToggle] } - return newState }) + + if (!subcategory) { + setExpandedItems((prev) => ({ ...prev, [category]: true })) + } } const toggleExpand = (category: string): void => { @@ -182,6 +158,10 @@ const CategoryDisplay: React.FC = ({ checkedItems, setChec ) } + const getSelectedSubcategoriesCount = (category: string): number => { + return checkedItems.filter((item) => item.startsWith(`${category}_`)).length + } + return (
) } + export default CategoryDisplay diff --git a/src/components/ecosystem/SelectedCategories.tsx b/src/components/ecosystem/SelectedCategories.tsx index 5540e8ad..a26a3807 100644 --- a/src/components/ecosystem/SelectedCategories.tsx +++ b/src/components/ecosystem/SelectedCategories.tsx @@ -23,15 +23,13 @@ import React from 'react' import { cmn, cls, styles } from '@skalenetwork/metaport' - import { Chip, Typography, Box } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' -import { CheckedItems } from '../../core/ecosystem/categoryUtils' import { categories, Category, Subcategory } from '../../core/ecosystem/categories' interface SelectedCategoriesProps { - checkedItems: CheckedItems - setCheckedItems: React.Dispatch> + checkedItems: string[] + setCheckedItems: React.Dispatch> filteredAppsCount: number } @@ -62,25 +60,12 @@ const SelectedCategories: React.FC = ({ setCheckedItems, filteredAppsCount }) => { - const handleDelete = (category: string, fullSubcategory?: string) => { - setCheckedItems((prev) => { - const newCheckedItems = { ...prev } - if (fullSubcategory) { - delete newCheckedItems[fullSubcategory] - } else { - Object.keys(newCheckedItems).forEach((key) => { - if (key.startsWith(`${category}_`)) { - delete newCheckedItems[key] - } - }) - delete newCheckedItems[category] - } - return newCheckedItems - }) + const handleDelete = (itemToDelete: string) => { + setCheckedItems((prev) => prev.filter((item) => item !== itemToDelete)) } const clearAll = () => { - setCheckedItems({}) + setCheckedItems([]) } const getCategoryName = (key: string): string => categories[key]?.name || key @@ -94,25 +79,23 @@ const SelectedCategories: React.FC = ({ return subcategoryKey } - const selectedItems = Object.entries(checkedItems).filter(([_, value]) => value) - - if (selectedItems.length === 0) return + if (checkedItems.length === 0) return null return ( - {selectedItems.map(([key, _]) => { - const [category, subcategory] = key.split('_') + {checkedItems.map((item) => { + const [category, subcategory] = item.split('_') return ( } - onDelete={() => handleDelete(category, key)} + onDelete={() => handleDelete(item)} deleteIcon={} className={cls(cmn.mri10, 'outlined', cmn.p600)} /> diff --git a/src/components/ecosystem/SubcategoryList.tsx b/src/components/ecosystem/SubcategoryList.tsx index 6c909fe2..52f3b608 100644 --- a/src/components/ecosystem/SubcategoryList.tsx +++ b/src/components/ecosystem/SubcategoryList.tsx @@ -21,17 +21,18 @@ */ import React from 'react' -import Checkbox from '@mui/material/Checkbox' -import FormControlLabel from '@mui/material/FormControlLabel' -import { type Subcategory } from '../../core/ecosystem/categories' -import { type CheckedItems } from '../../core/ecosystem/categoryUtils' +import { FormControlLabel, Checkbox } from '@mui/material' +import { cls, cmn } from '@skalenetwork/metaport' import { highlightMatch } from './SearchBar' -import { cmn, cls } from '@skalenetwork/metaport' + +interface Subcategory { + name: string +} interface SubcategoryListProps { category: string subcategories: Record - checkedItems: CheckedItems + checkedItems: string[] onCheck: (category: string, subcategory: string) => void searchTerm: string } @@ -52,7 +53,7 @@ const SubcategoryList: React.FC = ({ { onCheck(category, shortName) }} diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts index 6fe37a94..ede695b7 100644 --- a/src/core/ecosystem/apps.ts +++ b/src/core/ecosystem/apps.ts @@ -27,10 +27,6 @@ export interface AppWithChainAndName extends types.AppMetadata { appName: string } -interface CategoryFilter { - [key: string]: boolean -} - export function getAllApps(chainsMetadata: types.ChainsMetadataMap): AppWithChainAndName[] { const allApps: AppWithChainAndName[] = [] @@ -55,22 +51,21 @@ export function sortAppsByAlias(apps: AppWithChainAndName[]): AppWithChainAndNam export function filterAppsByCategory( apps: AppWithChainAndName[], - filter: CategoryFilter + checkedItems: string[] ): AppWithChainAndName[] { - if (Object.keys(filter).length === 0) return apps + if (checkedItems.length === 0) return apps return apps.filter((app) => { if (!app.categories || Object.keys(app.categories).length === 0) return false return Object.entries(app.categories).some(([category, subcategories]) => { - // Check if the main category is selected in the filter - if (filter[category] === true) return true + // Check if the main category is in checkedItems + if (checkedItems.includes(category)) return true // If the main category isn't selected, check subcategories if (Array.isArray(subcategories)) { return subcategories.some((subcategory) => { const subcategoryKey = `${category}_${subcategory}` - return filter[subcategoryKey] === true + return checkedItems.includes(subcategoryKey) }) } - return false }) }) diff --git a/src/core/ecosystem/categoryUtils.ts b/src/core/ecosystem/categoryUtils.ts index 87a7a1e0..912b9442 100644 --- a/src/core/ecosystem/categoryUtils.ts +++ b/src/core/ecosystem/categoryUtils.ts @@ -23,32 +23,27 @@ import { categories } from './categories' -export interface CheckedItems { - [key: string]: boolean -} - export interface ExpandedItems { [key: string]: boolean } -export const getSelectedSubcategoriesCount = ( - category: string, - checkedItems: CheckedItems -): number => { +export const getSelectedSubcategoriesCount = (category: string, checkedItems: string[]): number => { const subcategories = categories[category].subcategories if (typeof subcategories === 'object' && !Array.isArray(subcategories)) { - return Object.keys(subcategories).filter((subKey) => checkedItems[`${category}_${subKey}`]) - .length + return checkedItems.filter((item) => item.startsWith(`${category}_`)).length } return 0 } -export const getSelectedCategoriesCount = (checkedItems: CheckedItems): number => { - return Object.keys(categories).filter( - (category) => - checkedItems[category] || - Object.keys(checkedItems).some((key) => key.startsWith(`${category}_`)) - ).length +export const getSelectedCategoriesCount = (checkedItems: string[]): number => { + const selectedCategories = new Set() + + checkedItems.forEach((item) => { + const [category] = item.split('_') + selectedCategories.add(category) + }) + + return selectedCategories.size } export const filterCategories = (searchTerm: string) => { diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index 418975e1..0a996f51 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -25,20 +25,17 @@ import { Helmet } from 'react-helmet' import { useState, useEffect } from 'react' -import { cmn, cls, styles, type MetaportCore } from '@skalenetwork/metaport' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' import { type types } from '@/core' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import CircularProgress from '@mui/material/CircularProgress' -import EditRoundedIcon from '@mui/icons-material/EditRounded' -import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded' import HubsSection from '../components/HubsSection' import { getPrimaryCategory } from '../components/CategoryBadge' import { META_TAGS } from '../core/meta' -import { Button } from '@mui/material' import AppChains from '../components/AppChains' export default function Chains(props: { @@ -85,9 +82,7 @@ export default function Chains(props: {

Chains

- {props.isXs - ? 'Explore dApps, get block explorer links and endpoints' - : 'Explore SKALE Hubs, AppChains, connect, get block explorer links and endpoints'} + Chains info, block explorers and endpoints

- ) diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index da053770..b2d4e7d3 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import { useEffect, useState } from 'react' +import { useState, useEffect } from 'react' import { Helmet } from 'react-helmet' import Container from '@mui/material/Container' @@ -50,7 +50,7 @@ import { getAllApps, sortAppsByAlias } from '../core/ecosystem/apps' -import { CheckedItems } from '../core/ecosystem/categoryUtils' + import SearchComponent from '../components/ecosystem/AppSearch' import SelectedCategories from '../components/ecosystem/SelectedCategories' import SkStack from '../components/SkStack' @@ -61,7 +61,7 @@ export default function Ecosystem(props: { isXs: boolean }) { const allApps = sortAppsByAlias(getAllApps(props.chainsMeta)) - const [checkedItems, setCheckedItems] = useState({}) + const [checkedItems, setCheckedItems] = useState([]) const [apps, setApps] = useState([]) const [searchTerm, setSearchTerm] = useState('') @@ -73,8 +73,7 @@ export default function Ecosystem(props: { setApps(filterAppsBySearchTerm(filterAppsByCategory(allApps, checkedItems), searchTerm)) }, [checkedItems, searchTerm]) - console.log('render Ecosystem') - console.log(checkedItems) + console.log('render Ecosystem') // todo: optimize renders return ( @@ -108,7 +107,7 @@ export default function Ecosystem(props: { {}} + onChange={() => { }} scrollButtons="auto" className={cls( cmn.mbott20, diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index b274b2af..e03519ca 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -112,7 +112,7 @@ export default function Start(props: { } /> From 5736dd5807ef6d8305c5e60ce34793f4bfaec06a Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 7 Aug 2024 20:15:33 +0100 Subject: [PATCH 06/39] Update filtering logic, add url params --- src/components/ecosystem/Categories.tsx | 89 ++++++++----------- .../ecosystem/SelectedCategories.tsx | 17 ++-- src/core/ecosystem/urlParamsUtil.ts | 48 ++++++++++ src/pages/Ecosystem.tsx | 18 +++- 4 files changed, 109 insertions(+), 63 deletions(-) create mode 100644 src/core/ecosystem/urlParamsUtil.ts diff --git a/src/components/ecosystem/Categories.tsx b/src/components/ecosystem/Categories.tsx index 601c1ac7..16a770d9 100644 --- a/src/components/ecosystem/Categories.tsx +++ b/src/components/ecosystem/Categories.tsx @@ -20,11 +20,9 @@ * @copyright SKALE Labs 2024-Present */ -import React, { useState, useMemo, useRef, useEffect } from 'react' +import React, { useState, useMemo, useRef } from 'react' import { cmn, cls } from '@skalenetwork/metaport' - import { filterCategories } from '../../core/ecosystem/categoryUtils' - import Checkbox from '@mui/material/Checkbox' import FormControlLabel from '@mui/material/FormControlLabel' import IconButton from '@mui/material/IconButton' @@ -35,53 +33,56 @@ import Menu from '@mui/material/Menu' import Button from '@mui/material/Button' import ManageSearchRoundedIcon from '@mui/icons-material/ManageSearchRounded' import { categories, type Category } from '../../core/ecosystem/categories' - import SearchBar, { highlightMatch } from './SearchBar' import SubcategoryList from './SubcategoryList' interface CategoryDisplayProps { checkedItems: string[] - setCheckedItems: React.Dispatch> + setCheckedItems: (items: string[]) => void } const CategoryDisplay: React.FC = ({ checkedItems, setCheckedItems }) => { const [expandedItems, setExpandedItems] = useState>({}) const [anchorEl, setAnchorEl] = useState(null) const [searchTerm, setSearchTerm] = useState('') - const [buttonWidth, setButtonWidth] = useState(undefined) const buttonRef = useRef(null) - useEffect(() => { - if (buttonRef.current != null) { - setButtonWidth(buttonRef.current.offsetWidth) - } - }, []) + const filteredCategories = useMemo(() => filterCategories(searchTerm), [searchTerm]) const handleCheck = (category: string, subcategory: string | null = null): void => { - setCheckedItems((prev) => { - let newCheckedItems = [...prev] - const itemToToggle = subcategory ? `${category}_${subcategory}` : category + const newCheckedItems = [...checkedItems] + const itemToToggle = subcategory ? `${category}_${subcategory}` : category + + if (newCheckedItems.includes(itemToToggle)) { + const index = newCheckedItems.indexOf(itemToToggle) + newCheckedItems.splice(index, 1) - if (newCheckedItems.includes(itemToToggle)) { - return newCheckedItems.filter((item) => item !== itemToToggle) + if (!subcategory) { + setExpandedItems((prev) => ({ ...prev, [category]: false })) + } + } else { + if (subcategory) { + const mainCategoryIndex = newCheckedItems.indexOf(category) + if (mainCategoryIndex !== -1) { + newCheckedItems.splice(mainCategoryIndex, 1) + } + newCheckedItems.push(itemToToggle) } else { - if (subcategory) { - // Remove the main category if it was checked - const mainCategoryIndex = newCheckedItems.indexOf(category) - if (mainCategoryIndex !== -1) { - newCheckedItems.splice(mainCategoryIndex, 1) + const subcategoriesToRemove = newCheckedItems.filter((item) => + item.startsWith(`${category}_`) + ) + subcategoriesToRemove.forEach((item) => { + const index = newCheckedItems.indexOf(item) + if (index !== -1) { + newCheckedItems.splice(index, 1) } - } else { - // Remove all subcategories of this category - newCheckedItems = newCheckedItems.filter((item) => !item.startsWith(`${category}_`)) - } - return [...newCheckedItems, itemToToggle] + }) + newCheckedItems.push(category) + setExpandedItems((prev) => ({ ...prev, [category]: true })) } - }) - - if (!subcategory) { - setExpandedItems((prev) => ({ ...prev, [category]: true })) } + + setCheckedItems(newCheckedItems) } const toggleExpand = (category: string): void => { @@ -100,23 +101,7 @@ const CategoryDisplay: React.FC = ({ checkedItems, setChec } const handleSearch = (event: React.ChangeEvent): void => { - const newSearchTerm = event.target.value - setSearchTerm(newSearchTerm) - - const newExpandedItems = { ...expandedItems } - Object.entries(categories).forEach(([shortName, data]) => { - const subs = data.subcategories - if (typeof subs === 'object' && !Array.isArray(subs)) { - const hasMatchingSubcategory = Object.values(subs).some((sub) => - sub.name.toLowerCase().includes(newSearchTerm.toLowerCase()) - ) - const categoryMatches = data.name.toLowerCase().includes(newSearchTerm.toLowerCase()) - - newExpandedItems[shortName] = - hasMatchingSubcategory || (categoryMatches && newSearchTerm !== '') - } - }) - setExpandedItems(newExpandedItems) + setSearchTerm(event.target.value) } const handleClearSearch = (): void => { @@ -124,7 +109,9 @@ const CategoryDisplay: React.FC = ({ checkedItems, setChec setExpandedItems({}) } - const filteredCategories = useMemo(() => filterCategories(searchTerm), [searchTerm]) + const getSelectedSubcategoriesCount = (category: string): number => { + return checkedItems.filter((item) => item.startsWith(`${category}_`)).length + } const renderSubcategories = (shortName: string, data: Category): JSX.Element | null => { const subs = data.subcategories @@ -158,10 +145,6 @@ const CategoryDisplay: React.FC = ({ checkedItems, setChec ) } - const getSelectedSubcategoriesCount = (category: string): number => { - return checkedItems.filter((item) => item.startsWith(`${category}_`)).length - } - return (
diff --git a/src/components/ecosystem/Categories.tsx b/src/components/ecosystem/Categories.tsx index 6fe6b914..b714abfe 100644 --- a/src/components/ecosystem/Categories.tsx +++ b/src/components/ecosystem/Categories.tsx @@ -22,7 +22,7 @@ import React, { useState, useMemo, useRef } from 'react' import { cmn, cls } from '@skalenetwork/metaport' -import { filterCategories } from '../../core/ecosystem/categoryUtils' +import { filterCategories } from '../../core/ecosystem/utils' import Checkbox from '@mui/material/Checkbox' import FormControlLabel from '@mui/material/FormControlLabel' import IconButton from '@mui/material/IconButton' diff --git a/src/components/ecosystem/FavoriteApps.tsx b/src/components/ecosystem/FavoriteApps.tsx new file mode 100644 index 00000000..9f1c840b --- /dev/null +++ b/src/components/ecosystem/FavoriteApps.tsx @@ -0,0 +1,30 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file FavoriteApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' + +const FavoriteApps: React.FC = () => { + return
Favorite Apps Component (To be implemented)
+} + +export default FavoriteApps diff --git a/src/components/ecosystem/NewApps.tsx b/src/components/ecosystem/NewApps.tsx new file mode 100644 index 00000000..6bd0cc29 --- /dev/null +++ b/src/components/ecosystem/NewApps.tsx @@ -0,0 +1,62 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file NewApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { Grid } from '@mui/material' +import { cls } from '@skalenetwork/metaport' +import { type types } from '@/core' +import AppCard from './AppCardV2' + +interface NewAppsProps { + newApps: { chain: string; app: string; added: number }[] + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap +} + +const NewApps: React.FC = ({ newApps, skaleNetwork, chainsMeta }) => { + return ( + + {newApps.map((app) => ( + + + + ))} + + ) +} + +export default NewApps diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx new file mode 100644 index 00000000..67e2e6b1 --- /dev/null +++ b/src/components/ecosystem/TrendingApps.tsx @@ -0,0 +1,30 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file TrendingApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' + +const TrendingApps: React.FC = () => { + return
Trending Apps Component (To be implemented)
+} + +export default TrendingApps diff --git a/src/core/constants.ts b/src/core/constants.ts index 30e32738..9711453f 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -82,3 +82,5 @@ export const STATS_API: { [key in types.SkaleNetwork]: string | null } = { export const BASE_METADATA_URL = 'https://raw.githubusercontent.com/skalenetwork/skale-network/add-new-categories/metadata/' // todo: tmp + +export const MAX_APPS_DEFAULT = 12 diff --git a/src/core/ecosystem/categoryUtils.ts b/src/core/ecosystem/utils.ts similarity index 69% rename from src/core/ecosystem/categoryUtils.ts rename to src/core/ecosystem/utils.ts index 912b9442..5a16bea6 100644 --- a/src/core/ecosystem/categoryUtils.ts +++ b/src/core/ecosystem/utils.ts @@ -17,10 +17,11 @@ */ /** - * @file categoryUtils.tsx + * @file utils.tsx * @copyright SKALE Labs 2024-Present */ +import { type types } from '@/core' import { categories } from './categories' export interface ExpandedItems { @@ -58,3 +59,33 @@ export const filterCategories = (searchTerm: string) => { return categoryMatch || subcategoryMatch }) } + +export const getRecentApps = ( + chainsMeta: types.ChainsMetadataMap, + count: number = 12 +): types.AppWithTimestamp[] => { + const appsWithTimestamp: types.AppWithTimestamp[] = [] + + Object.entries(chainsMeta).forEach(([chainName, chainData]) => { + if (chainData.apps) { + Object.entries(chainData.apps).forEach(([appName, appData]) => { + if (appData.added) { + appsWithTimestamp.push({ + chain: chainName, + app: appName, + added: appData.added + }) + } + }) + } + }) + + return appsWithTimestamp.sort((a, b) => b.added - a.added).slice(0, count) +} + +export const isNewApp = ( + app: { chain: string; app: string }, + newApps: types.AppWithTimestamp[] +): boolean => { + return newApps.some((newApp) => newApp.chain === app.chain && newApp.app === app.app) +} diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 38c3ad17..6ea821aa 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -21,13 +21,13 @@ * @copyright SKALE Labs 2024-Present */ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { Helmet } from 'react-helmet' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' -import Grid from '@mui/material/Grid' +import { Button, Tab, Tabs } from '@mui/material' import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded' import EditRoundedIcon from '@mui/icons-material/EditRounded' @@ -37,11 +37,8 @@ import StarRoundedIcon from '@mui/icons-material/StarRounded' import { type types } from '@/core' -import AppCard from '../components/ecosystem/AppCardV2' - import { cmn, cls, type MetaportCore, styles } from '@skalenetwork/metaport' import { META_TAGS } from '../core/meta' -import { Button, Tab, Tabs } from '@mui/material' import CategoryDisplay from '../components/ecosystem/Categories' import { AppWithChainAndName, @@ -55,6 +52,12 @@ import SearchComponent from '../components/ecosystem/AppSearch' import SelectedCategories from '../components/ecosystem/SelectedCategories' import SkStack from '../components/SkStack' import { useUrlParams } from '../core/ecosystem/urlParamsUtil' +import { getRecentApps } from '../core/ecosystem/utils' + +import AllApps from '../components/ecosystem/AllApps' +import NewApps from '../components/ecosystem/NewApps' +import FavoriteApps from '../components/ecosystem/FavoriteApps' +import TrendingApps from '../components/ecosystem/TrendingApps' export default function Ecosystem(props: { mpc: MetaportCore @@ -62,10 +65,13 @@ export default function Ecosystem(props: { isXs: boolean }) { const { getCheckedItemsFromUrl, setCheckedItemsInUrl } = useUrlParams() - const allApps = sortAppsByAlias(getAllApps(props.chainsMeta)) + const allApps = useMemo(() => sortAppsByAlias(getAllApps(props.chainsMeta)), [props.chainsMeta]) const [checkedItems, setCheckedItems] = useState([]) - const [apps, setApps] = useState([]) + const [filteredApps, setFilteredApps] = useState([]) const [searchTerm, setSearchTerm] = useState('') + const [activeTab, setActiveTab] = useState(0) + + const newApps = useMemo(() => getRecentApps(props.chainsMeta, 12), [props.chainsMeta]) useEffect(() => { const initialCheckedItems = getCheckedItemsFromUrl() @@ -73,19 +79,26 @@ export default function Ecosystem(props: { }, []) useEffect(() => { - setApps(allApps) - }, [props.chainsMeta]) - - useEffect(() => { - setApps(filterAppsBySearchTerm(filterAppsByCategory(allApps, checkedItems), searchTerm)) - }, [checkedItems, searchTerm]) + const filtered = filterAppsBySearchTerm(filterAppsByCategory(allApps, checkedItems), searchTerm) + setFilteredApps(filtered) + }, [allApps, checkedItems, searchTerm]) const handleSetCheckedItems = (newCheckedItems: string[]) => { setCheckedItems(newCheckedItems) setCheckedItemsInUrl(newCheckedItems) } - console.log('render Ecosystem') // todo: optimize renders + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue) + } + + const filteredNewApps = useMemo(() => { + return newApps.filter((app) => + filteredApps.some( + (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.app + ) + ) + }, [newApps, filteredApps]) return ( @@ -114,12 +127,12 @@ export default function Ecosystem(props: { {}} + value={activeTab} + onChange={handleTabChange} scrollButtons="auto" className={cls( cmn.mbott20, @@ -153,26 +166,24 @@ export default function Ecosystem(props: { /> - - {apps.map((app) => ( - - - - ))} - + {activeTab === 0 && ( + + )} + {activeTab === 1 && ( + + )} + {activeTab === 2 && } + {activeTab === 3 && } +
diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index b12aa323..d98969d9 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -42,9 +42,11 @@ import OutboundRoundedIcon from '@mui/icons-material/OutboundRounded' import PageCard from '../components/PageCard' import AppCard from '../components/ecosystem/AppCardV2' -import { useEffect, useState } from 'react' -import FeaturedApps from '../components/FeaturedApps' +import { useEffect, useMemo, useState } from 'react' import CategoryCardsGrid from '../components/ecosystem/CategoryCardsGrid' +import { getRecentApps } from '../core/ecosystem/utils' +import { MAX_APPS_DEFAULT } from '../core/constants' +import NewApps from '../components/ecosystem/NewApps' export default function Start(props: { isXs: boolean @@ -54,6 +56,10 @@ export default function Start(props: { chainsMeta: types.ChainsMetadataMap }) { const [_, setIntervalId] = useState() + const newApps = useMemo( + () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), + [props.chainsMeta] + ) useEffect(() => { props.loadData() @@ -133,7 +139,11 @@ export default function Start(props: {

New dApps on SKALE

- +
From 80c15be35fce4677bc76fcc7ff6ca0ef8c883c8a Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 8 Aug 2024 21:52:01 +0100 Subject: [PATCH 12/39] Add Carousel component, update categories --- src/App.scss | 11 +++ src/components/Carousel.tsx | 113 ++++++++++++++++++++++ src/components/ecosystem/AppCardV2.tsx | 2 +- src/components/ecosystem/CategoryCard.tsx | 6 +- src/components/ecosystem/NewApps.tsx | 53 ++++++---- src/core/ecosystem/categories.ts | 44 ++++----- src/pages/Start.tsx | 18 ++-- 7 files changed, 188 insertions(+), 59 deletions(-) create mode 100644 src/components/Carousel.tsx diff --git a/src/App.scss b/src/App.scss index facbea6e..f445caa1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -366,6 +366,17 @@ body::-webkit-scrollbar { width: 100%; } +.skArrows { + .MuiIconButton-root { + svg { + color: white; + font-size: 14pt; + padding: 2px; + } + } + +} + .btn { text-transform: none !important; font-size: 0.8025rem !important; diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx new file mode 100644 index 00000000..eb01cd02 --- /dev/null +++ b/src/components/Carousel.tsx @@ -0,0 +1,113 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Carousel.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useState, ReactNode } from 'react' +import { Box, IconButton, useTheme, useMediaQuery } from '@mui/material' + +import ArrowBackIosRoundedIcon from '@mui/icons-material/ArrowBackIosRounded' +import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' + +import { cmn, cls } from '@skalenetwork/metaport' + +interface CarouselProps { + children: ReactNode[] + showArrows?: boolean +} + +const Carousel: React.FC = ({ children, showArrows = true }) => { + const [startIndex, setStartIndex] = useState(0) + const theme = useTheme() + const isXs = useMediaQuery(theme.breakpoints.only('xs')) + const isSm = useMediaQuery(theme.breakpoints.only('sm')) + const isMd = useMediaQuery(theme.breakpoints.only('md')) + + let itemsToShow = 3 // Default for lg and up + if (isXs) { + itemsToShow = 1 + } else if (isSm) { + itemsToShow = 2 + } else if (isMd) { + itemsToShow = 3 + } + + const handlePrev = () => { + setStartIndex((prevIndex) => Math.max(0, prevIndex - 1)) + } + + const handleNext = () => { + setStartIndex((prevIndex) => Math.min(children.length - itemsToShow, prevIndex + 1)) + } + + const visibleChildren = children.slice(startIndex, startIndex + itemsToShow) + + return ( + + + {visibleChildren.map((child, index) => ( + + {child} + + ))} + + {showArrows && children.length > itemsToShow && ( + + + + + = children.length - itemsToShow} + size="small" + className={cls(cmn.pSec, 'outlined', cmn.mleft5)} + > + + + + )} + + ) +} + +export default Carousel diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 9d994408..08125403 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -81,7 +81,7 @@ export default function AppCard(props: {

{getChainAlias(props.chainsMeta, props.schainName, props.appName)}

- {isNew && } + {isNew && } {appMeta.tags?.includes('pretge') && ( )} diff --git a/src/components/ecosystem/CategoryCard.tsx b/src/components/ecosystem/CategoryCard.tsx index 9ee26118..d1131c02 100644 --- a/src/components/ecosystem/CategoryCard.tsx +++ b/src/components/ecosystem/CategoryCard.tsx @@ -23,8 +23,7 @@ import React from 'react' import { Link } from 'react-router-dom' -import { cmn, cls, styles, SkPaper } from '@skalenetwork/metaport' -import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded' +import { cmn, cls, SkPaper } from '@skalenetwork/metaport' interface CategoryCardProps { categoryName: string @@ -47,9 +46,6 @@ const CategoryCard: React.FC = ({ categoryName, fullName, pro {projectCount} project{projectCount !== 1 ? 's' : ''}

-
- -
diff --git a/src/components/ecosystem/NewApps.tsx b/src/components/ecosystem/NewApps.tsx index 6bd0cc29..549c4228 100644 --- a/src/components/ecosystem/NewApps.tsx +++ b/src/components/ecosystem/NewApps.tsx @@ -22,37 +22,52 @@ */ import React from 'react' -import { Grid } from '@mui/material' +import { Grid, Box } from '@mui/material' import { cls } from '@skalenetwork/metaport' -import { type types } from '@/core' import AppCard from './AppCardV2' +import Carousel from '../Carousel' +import { type types } from '@/core' interface NewAppsProps { newApps: { chain: string; app: string; added: number }[] skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap + useCarousel?: boolean } -const NewApps: React.FC = ({ newApps, skaleNetwork, chainsMeta }) => { +const NewApps: React.FC = ({ + newApps, + skaleNetwork, + chainsMeta, + useCarousel = false +}) => { + const appCards = newApps.map((app) => ( + + + + )) + + if (useCarousel) { + return {appCards} + } + return ( {newApps.map((app) => ( - - + + + + ))} diff --git a/src/core/ecosystem/categories.ts b/src/core/ecosystem/categories.ts index 155ede4f..e311cce8 100644 --- a/src/core/ecosystem/categories.ts +++ b/src/core/ecosystem/categories.ts @@ -35,61 +35,53 @@ export interface Categories { } export const categories: Categories = { - 'hub-chains': { name: 'Hub Chains', subcategories: {} }, ai: { name: 'AI', subcategories: {} }, - 'bridges-onramps': { name: 'Bridges', subcategories: {} }, - cex: { name: 'CEX', subcategories: ['Add Later'] }, - consumer: { name: 'Consumer', subcategories: ['Add Later'] }, + bridges: { name: 'Bridges', subcategories: {} }, + dao: { name: 'DAO', subcategories: {} }, + 'data-information': { name: 'Data/Information', subcategories: {} }, defi: { name: 'DeFi', subcategories: { - aggregators: { name: 'Aggregators' }, - betting: { name: 'Betting' }, custody: { name: 'Custody' }, dex: { name: 'DEX' }, - launchpads: { name: 'Launchpads' }, - lending: { name: 'Lending' }, - lottery: { name: 'Lottery' }, - options: { name: 'Options' }, - payments: { name: 'Payments' }, - 'perps-derivatives': { name: 'Perps + Derivatives' }, - 'prediction-markets': { name: 'Prediction Markets' }, - stablecoins: { name: 'Stablecoins' }, - synthetics: { name: 'Synthetics' }, yield: { name: 'Yield' } } }, + 'digital-collectibles': { name: 'Digital Collectibles', subcategories: {} }, + entertainment: { name: 'Entertainment', subcategories: {} }, + explorer: { name: 'Explorer', subcategories: {} }, gaming: { name: 'Gaming', subcategories: { 'action-adventure': { name: 'Action/Adventure' }, 'battle-royale': { name: 'Battle Royale' }, + 'cards-deck-building': { name: 'Cards + Deck Building' }, casual: { name: 'Casual' }, - 'cards_deck-building': { name: 'Cards + Deck Building' }, console: { name: 'Console' }, fighting: { name: 'Fighting' }, - shooter: { name: 'Shooter' }, metaverse: { name: 'Metaverse' }, - mmorpg: { name: 'MMORPG' }, mobile: { name: 'Mobile' }, + mmorpg: { name: 'MMORPG' }, pc: { name: 'PC' }, - browser: { name: 'Browser' }, platformer: { name: 'Platformer' }, puzzle: { name: 'Puzzle' }, racing: { name: 'Racing' }, rpg: { name: 'RPG' }, sandbox: { name: 'Sandbox' }, + shooter: { name: 'Shooter' }, simulation: { name: 'Simulation' }, sports: { name: 'Sports' }, strategy: { name: 'Strategy' } } }, + hub: { name: 'Hub', subcategories: {} }, infrastructure: { name: 'Infrastructure', subcategories: {} }, - mobile: { name: 'Mobile', subcategories: {} }, - oracles: { name: 'Oracles', subcategories: {} }, - wallets: { name: 'Wallets', subcategories: {} }, - analytics: { name: 'Analytics', subcategories: {} }, - daos: { name: 'DAOs', subcategories: {} }, - socialfi: { name: 'SocialFi', subcategories: {} }, - nfts: { name: 'NFTs', subcategories: {} } + nfts: { name: 'NFTs', subcategories: {} }, + oracle: { name: 'Oracle', subcategories: {} }, + other: { name: 'Other', subcategories: {} }, + partner: { name: 'Partner', subcategories: {} }, + security: { name: 'Security', subcategories: {} }, + 'social-network': { name: 'Social Network', subcategories: {} }, + tools: { name: 'Tools', subcategories: {} }, + wallet: { name: 'Wallet', subcategories: {} } } diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index d98969d9..17cb7a59 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -47,6 +47,7 @@ import CategoryCardsGrid from '../components/ecosystem/CategoryCardsGrid' import { getRecentApps } from '../core/ecosystem/utils' import { MAX_APPS_DEFAULT } from '../core/constants' import NewApps from '../components/ecosystem/NewApps' +import Carousel from '../components/Carousel' export default function Start(props: { isXs: boolean @@ -80,17 +81,20 @@ export default function Start(props: { : null if (apps) { - appCards = apps.slice(0, 3).map((topApp: types.IAppId) => ( - + appCards = apps.slice(0, 11).map( + ( + topApp: types.IAppId // todo: use max apps! + ) => ( - - )) + ) + ) } return ( @@ -143,16 +147,14 @@ export default function Start(props: { newApps={newApps} skaleNetwork={props.skaleNetwork} chainsMeta={props.chainsMeta} + useCarousel={true} />

Trending dApps on SKALE

- - - {appCards} - + {appCards}
From dab41c2b2ad278d6237ed749c26cacc02706ffa1 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 8 Aug 2024 22:19:57 +0100 Subject: [PATCH 13/39] Add See all buttons, minor fixes --- src/App.scss | 2 +- src/components/ecosystem/AllApps.tsx | 2 +- src/pages/Start.tsx | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/App.scss b/src/App.scss index f445caa1..7ff74a13 100644 --- a/src/App.scss +++ b/src/App.scss @@ -391,7 +391,7 @@ body::-webkit-scrollbar { .btnSm { text-transform: none !important; - font-size: 0.8025rem !important; + font-size: 0.7025rem !important; line-height: 1.5 !important; letter-spacing: 0.02857em !important; font-weight: 600 !important; diff --git a/src/components/ecosystem/AllApps.tsx b/src/components/ecosystem/AllApps.tsx index 29e2bf81..3c9a16e3 100644 --- a/src/components/ecosystem/AllApps.tsx +++ b/src/components/ecosystem/AllApps.tsx @@ -40,7 +40,7 @@ const AllApps: React.FC = ({ apps, skaleNetwork, chainsMeta, newAp {apps.map((app) => ( -

New dApps on SKALE

+

New dApps on SKALE

+
-
-

Trending dApps on SKALE

+

Trending dApps on SKALE

+
{appCards} From b865bc046ca4b4c026a9534ac9059fbd274227d0 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 9 Aug 2024 12:13:24 +0100 Subject: [PATCH 14/39] Store tab in the URL, add links to start page --- src/core/ecosystem/urlParamsUtil.ts | 19 ++++++++++++++++++- src/pages/Ecosystem.tsx | 6 +++++- src/pages/Start.tsx | 20 +++++++++++++++----- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/core/ecosystem/urlParamsUtil.ts b/src/core/ecosystem/urlParamsUtil.ts index 1460a153..0c4dba5e 100644 --- a/src/core/ecosystem/urlParamsUtil.ts +++ b/src/core/ecosystem/urlParamsUtil.ts @@ -44,5 +44,22 @@ export const useUrlParams = () => { [searchParams, setSearchParams] ) - return { getCheckedItemsFromUrl, setCheckedItemsInUrl } + const getTabIndexFromUrl = () => { + const params = new URLSearchParams(window.location.search) + const tabIndex = params.get('tab') + return tabIndex ? parseInt(tabIndex, 10) : 0 + } + + const setTabIndexInUrl = (tabIndex: number) => { + const params = new URLSearchParams(window.location.search) + params.set('tab', tabIndex.toString()) + window.history.replaceState({}, '', `${window.location.pathname}?${params}`) + } + + return { + getCheckedItemsFromUrl, + setCheckedItemsInUrl, + getTabIndexFromUrl, + setTabIndexInUrl + } } diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 6ea821aa..bae5f1d0 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -64,7 +64,8 @@ export default function Ecosystem(props: { chainsMeta: types.ChainsMetadataMap isXs: boolean }) { - const { getCheckedItemsFromUrl, setCheckedItemsInUrl } = useUrlParams() + const { getCheckedItemsFromUrl, setCheckedItemsInUrl, getTabIndexFromUrl, setTabIndexInUrl } = + useUrlParams() const allApps = useMemo(() => sortAppsByAlias(getAllApps(props.chainsMeta)), [props.chainsMeta]) const [checkedItems, setCheckedItems] = useState([]) const [filteredApps, setFilteredApps] = useState([]) @@ -76,6 +77,8 @@ export default function Ecosystem(props: { useEffect(() => { const initialCheckedItems = getCheckedItemsFromUrl() setCheckedItems(initialCheckedItems) + const initialTabIndex = getTabIndexFromUrl() + setActiveTab(initialTabIndex) }, []) useEffect(() => { @@ -90,6 +93,7 @@ export default function Ecosystem(props: { const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { setActiveTab(newValue) + setTabIndexInUrl(newValue) } const filteredNewApps = useMemo(() => { diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index 1e06f33d..235b53ef 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -21,6 +21,8 @@ * @copyright SKALE Labs 2023-Present */ +import { Link } from 'react-router-dom' + import { cmn, cls } from '@skalenetwork/metaport' import { type types } from '@/core' @@ -28,6 +30,7 @@ import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' import Grid from '@mui/material/Grid' +import { Button } from '@mui/material' import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' @@ -48,7 +51,6 @@ import { getRecentApps } from '../core/ecosystem/utils' import { MAX_APPS_DEFAULT } from '../core/constants' import NewApps from '../components/ecosystem/NewApps' import Carousel from '../components/Carousel' -import { Button } from '@mui/material' export default function Start(props: { isXs: boolean @@ -142,8 +144,12 @@ export default function Start(props: {
-

New dApps on SKALE

- +

+ New dApps on SKALE +

+ + +
-

Trending dApps on SKALE

- +

+ Trending dApps on SKALE +

+ + +
{appCards} From 9ad87c661f4fa395aee7a497e774d61e162eabe4 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 9 Aug 2024 17:59:24 +0100 Subject: [PATCH 15/39] Handle offchain apps, update socials component, move app pages under ecosystem, update menu --- src/App.scss | 13 ++ src/Header.tsx | 12 +- src/Router.tsx | 10 +- src/components/AppCard.tsx | 2 +- src/components/Breadcrumbs.tsx | 4 +- src/components/ecosystem/AppCardV2.tsx | 8 +- src/components/ecosystem/Socials.tsx | 173 ++++++++++--------------- src/core/constants.ts | 2 + src/core/ecosystem/categories.ts | 2 +- src/pages/App.tsx | 98 +++++++------- src/pages/Ecosystem.tsx | 3 +- 11 files changed, 159 insertions(+), 168 deletions(-) diff --git a/src/App.scss b/src/App.scss index 7ff74a13..6f5db1cf 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1264,6 +1264,15 @@ a, } } +.socialIconMd { + width: 40px !important; + height: 40px !important; + svg { + width: 40px !important; + height: 40px !important; + } +} + .pSec { color: RGB(255 255 255 / 65%); @@ -1283,6 +1292,10 @@ a, } } +.bgBlack { + background: black !important; +} + .bg { background: $sk-bg !important; diff --git a/src/Header.tsx b/src/Header.tsx index 5af0a553..94e4654e 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -34,7 +34,8 @@ import MoreMenu from './components/MoreMenu' import AccountMenu from './components/AccountMenu' import NetworkSwitch from './components/NetworkSwitch' -import { MAINNET_CHAIN_NAME, MAIN_SKALE_URL } from './core/constants' +import { MAINNET_CHAIN_NAME } from './core/constants' +import { Link } from 'react-router-dom' export default function Header(props: { address: `0x${string}` | undefined; mpc: MetaportCore }) { return ( @@ -46,14 +47,9 @@ export default function Header(props: { address: `0x${string}` | undefined; mpc: >
- + logo - +
{MAINNET_CHAIN_NAME !== 'mainnet' ? ( diff --git a/src/Router.tsx b/src/Router.tsx index 3d8b5732..d7121084 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -279,6 +279,12 @@ export default function Router() { /> } /> + + } + /> + - } - /> } /> } /> diff --git a/src/components/AppCard.tsx b/src/components/AppCard.tsx index 20cab988..f2b68c03 100644 --- a/src/components/AppCard.tsx +++ b/src/components/AppCard.tsx @@ -40,7 +40,7 @@ export default function AppCard(props: { transactions?: number }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) - const url = `/chains/${shortAlias}/${props.appName}` + const url = `/ecosystem/${shortAlias}/${props.appName}` return (
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 6a3a748f..179d785c 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -39,13 +39,13 @@ export default function Breadcrumbs(props: { sections: BreadcrumbSection[]; clas
{section.url ? ( - ) : ( - diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 08125403..d41d89f4 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -26,7 +26,7 @@ import { cmn, cls, SkPaper, ChainIcon } from '@skalenetwork/metaport' import { type types } from '@/core' import ChainLogo from '../ChainLogo' -import { MAINNET_CHAIN_LOGOS } from '../../core/constants' +import { MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../../core/constants' import { getChainShortAlias } from '../../core/chain' import { chainBg, getChainAlias } from '../../core/metadata' @@ -45,7 +45,7 @@ export default function AppCard(props: { newApps?: types.AppWithTimestamp[] }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) - const url = `/chains/${shortAlias}/${props.appName}` + const url = `/ecosystem/${shortAlias}/${props.appName}` const appMeta = props.chainsMeta[props.schainName]?.apps?.[props.appName]! const isNew = props.newApps && isNewApp({ chain: props.schainName, app: props.appName }, props.newApps) @@ -73,7 +73,9 @@ export default function AppCard(props: {
- + {props.schainName !== OFFCHAIN_APP && ( + + )}
diff --git a/src/components/ecosystem/Socials.tsx b/src/components/ecosystem/Socials.tsx index 79723c55..43eec15c 100644 --- a/src/components/ecosystem/Socials.tsx +++ b/src/components/ecosystem/Socials.tsx @@ -22,128 +22,99 @@ */ import React from 'react' -import { type types } from '@/core' - import { IconButton, Tooltip } from '@mui/material' import { LanguageRounded, FavoriteBorderOutlined, WavesRounded } from '@mui/icons-material' -import { cmn, cls } from '@skalenetwork/metaport' - import { SocialIcon } from 'react-social-icons/component' import 'react-social-icons/discord' import 'react-social-icons/github' import 'react-social-icons/telegram' import 'react-social-icons/x' +import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' interface SocialButtonsProps { social?: types.AppSocials onAddToFavorites?: () => void isFavorite?: boolean className?: string + size?: 'sm' | 'md' } const SocialButtons: React.FC = ({ social, onAddToFavorites, isFavorite = false, - className + className, + size = 'sm' }) => { - if (!social) return undefined + const isMd = size === 'md' + + const socialLinks = [ + { + key: 'website', + icon: ( + + ), + title: 'Website' + }, + { key: 'x', network: 'x', title: 'X (Twitter)' }, + { key: 'telegram', network: 'telegram', title: 'Telegram' }, + { key: 'discord', network: 'discord', title: 'Discord' }, + { key: 'github', network: 'github', title: 'GitHub' }, + { + key: 'swell', + icon: ( + + ), + title: 'Swell' + } + ] + return (
-
- {social.website && ( - - - - - - )} - {social.x && ( - - - - - - )} - {social.telegram && ( - - - - - - )} - {social.discord && ( - - - - - - )} - {social.github && ( - - - - - - )} - {social.swell && ( - - - - - - )} -
-
+ {social && ( +
+ {socialLinks.map(({ key, icon, network, title }) => { + const link = social[key as keyof types.AppSocials] + if (!link) return null + + return ( +
+ + + {icon || ( + + )} + + +
+ ) + })} +
+ )} + {!social &&
} + {!isMd && ( = ({ /> -
+ )}
) } diff --git a/src/core/constants.ts b/src/core/constants.ts index 9711453f..d4b2e2f0 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -84,3 +84,5 @@ export const BASE_METADATA_URL = 'https://raw.githubusercontent.com/skalenetwork/skale-network/add-new-categories/metadata/' // todo: tmp export const MAX_APPS_DEFAULT = 12 + +export const OFFCHAIN_APP = '__offchain' diff --git a/src/core/ecosystem/categories.ts b/src/core/ecosystem/categories.ts index e311cce8..1079a1c7 100644 --- a/src/core/ecosystem/categories.ts +++ b/src/core/ecosystem/categories.ts @@ -55,7 +55,7 @@ export const categories: Categories = { subcategories: { 'action-adventure': { name: 'Action/Adventure' }, 'battle-royale': { name: 'Battle Royale' }, - 'cards-deck-building': { name: 'Cards + Deck Building' }, + 'cards_deck-building': { name: 'Cards + Deck Building' }, casual: { name: 'Casual' }, console: { name: 'Console' }, fighting: { name: 'Fighting' }, diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 808b1a2a..9c36e480 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -56,7 +56,8 @@ import { findChainName } from '../core/chain' import { formatNumber } from '../core/timeHelper' import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' -import { DAPP_RADAR_BASE_URL, MAINNET_CHAIN_LOGOS } from '../core/constants' +import { DAPP_RADAR_BASE_URL, MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../core/constants' +import SocialButtons from '../components/ecosystem/Socials' export default function App(props: { mpc: MetaportCore @@ -73,15 +74,19 @@ export default function App(props: { const [counters, setCounters] = useState(null) chain = findChainName(props.chainsMeta, chain ?? '') + const chainMeta = props.chainsMeta[chain] + if (!chainMeta) return 'No such chain' const chainAlias = getChainAlias(props.chainsMeta, chain) const appAlias = getChainAlias(props.chainsMeta, chain, app) - const appMeta = props.chainsMeta[chain]?.apps?.[app]! + const appMeta = chainMeta.apps?.[app]! const appDescription = appMeta.description ?? 'No description' const dAppRadarUrl = `${DAPP_RADAR_BASE_URL}${appMeta.dappradar ?? app}` const expolorerUrl = getExplorerUrl(network, chain) + const isAppChain = chainMeta.apps && Object.keys(chainMeta.apps).length === 1 + useEffect(() => { props.loadData() }, []) @@ -123,14 +128,9 @@ export default function App(props: { , - url: '/chains' - }, - { - text: props.isXs ? 'Hub' : chainAlias, - icon: , - url: `/chains/${chain}` + url: '/ecosystem' }, { text: appAlias, @@ -226,50 +226,54 @@ export default function App(props: { icon={} /> ) : null} + +
- - } - > - - - {appMeta.contracts ? ( + {chain !== OFFCHAIN_APP && ( + } + handleChange={handleChange} + expanded={expanded} + panel="panel3" + title={`Runs on SKALE ${isAppChain ? 'Chain' : 'Hub'}`} + icon={} > -
- - {appMeta.contracts.map((contractAddress: string, index: number) => ( - - - - ))} - -
+
- ) : ( -
- )} -
+ {appMeta.contracts ? ( + } + > +
+ + {appMeta.contracts.map((contractAddress: string, index: number) => ( + + + + ))} + +
+
+ ) : ( +
+ )} +
+ )}
) diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index bae5f1d0..1d34f0d9 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -141,7 +141,8 @@ export default function Ecosystem(props: { className={cls( cmn.mbott20, [cmn.mtop20, Object.keys(checkedItems).length !== 0], - 'skTabs' + 'skTabs', + 'fwmobile' )} > Date: Fri, 9 Aug 2024 19:57:21 +0100 Subject: [PATCH 16/39] Update app page --- src/components/ecosystem/AppCardV2.tsx | 4 +- src/components/ecosystem/CategoriesShips.tsx | 11 +- src/pages/App.tsx | 154 ++++++++----------- src/styles/components.scss | 58 ++++++- 4 files changed, 127 insertions(+), 100 deletions(-) diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index d41d89f4..b57c8af8 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -53,10 +53,10 @@ export default function AppCard(props: { const appDescription = appMeta.description ?? 'No description' return ( - +
-
+
= ({ app, className return Object.entries(app.categories) .flatMap(([categoryTag, subcategories]) => [ - , + , ...(Array.isArray(subcategories) ? subcategories.map((subTag) => ( = ({ app, className )) : []) ]) - .slice(0, 3) // Limit to 3 chips + .slice(0, 3) // Limit to 3 chips // todo: remove restriction! }, [app.categories]) if (chips.length === 0) return null diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 9c36e480..7f5dac00 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -30,18 +30,15 @@ import { type types } from '@/core' import { Button, Grid } from '@mui/material' import Container from '@mui/material/Container' -import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' -import TrackChangesRoundedIcon from '@mui/icons-material/TrackChangesRounded' import ArticleRoundedIcon from '@mui/icons-material/ArticleRounded' import SavingsRoundedIcon from '@mui/icons-material/SavingsRounded' import DataSaverOffRoundedIcon from '@mui/icons-material/DataSaverOffRounded' import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' -import LinkRoundedIcon from '@mui/icons-material/LinkRounded' import WidgetsRoundedIcon from '@mui/icons-material/WidgetsRounded' -import InfoRoundedIcon from '@mui/icons-material/InfoRounded' +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' import HubRoundedIcon from '@mui/icons-material/HubRounded' +import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' -import ChainCategories from '../components/ChainCategories' import ChainLogo from '../components/ChainLogo' import SkStack from '../components/SkStack' import Tile from '../components/Tile' @@ -56,8 +53,9 @@ import { findChainName } from '../core/chain' import { formatNumber } from '../core/timeHelper' import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' -import { DAPP_RADAR_BASE_URL, MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../core/constants' +import { MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../core/constants' import SocialButtons from '../components/ecosystem/Socials' +import AppCategoriesChips from '../components/ecosystem/CategoriesShips' export default function App(props: { mpc: MetaportCore @@ -77,11 +75,10 @@ export default function App(props: { const chainMeta = props.chainsMeta[chain] if (!chainMeta) return 'No such chain' - const chainAlias = getChainAlias(props.chainsMeta, chain) const appAlias = getChainAlias(props.chainsMeta, chain, app) const appMeta = chainMeta.apps?.[app]! const appDescription = appMeta.description ?? 'No description' - const dAppRadarUrl = `${DAPP_RADAR_BASE_URL}${appMeta.dappradar ?? app}` + // const dAppRadarUrl = `${DAPP_RADAR_BASE_URL}${appMeta.dappradar ?? app}` const expolorerUrl = getExplorerUrl(network, chain) @@ -122,84 +119,67 @@ export default function App(props: { - - -
- , - url: '/ecosystem' - }, - { - text: appAlias, - icon: - } - ]} - /> -
+ +
+ , + url: '/ecosystem' + }, + { + text: appAlias, + icon: + } + ]} + /> +
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+ +
+ +

{appAlias}

+ + +
-
- - -
- - -
- - -

{appAlias}

- -
- } - /> - +
+ - - {appMeta.social?.website ? ( - - - - ) : null} - {appMeta.dappradar === undefined || appMeta.dappradar !== false ? ( - - - - ) : null} -
- } - /> {appMeta.contracts ? ( + ) : undefined } tooltip={ @@ -226,13 +206,11 @@ export default function App(props: { icon={} /> ) : null} - - + } />
-
{chain !== OFFCHAIN_APP && ( - + Date: Mon, 12 Aug 2024 13:27:16 +0100 Subject: [PATCH 17/39] Update interfaces, update socials section, update AllApps component --- .gitignore | 3 ++- packages/core/src/types/ChainsMetadata.ts | 4 +--- src/components/ecosystem/AllApps.tsx | 26 +++++++++++++++-------- src/components/ecosystem/Socials.tsx | 17 ++++++++++++++- src/core/constants.ts | 3 +++ src/pages/Ecosystem.tsx | 13 ++++++------ src/pages/Start.tsx | 16 +++----------- 7 files changed, 48 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 96dac237..a589be2f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ public/sitemap.xml public/robots.txt packages/*/node_modules -packages/*/dist \ No newline at end of file +packages/*/dist +tools/* \ No newline at end of file diff --git a/packages/core/src/types/ChainsMetadata.ts b/packages/core/src/types/ChainsMetadata.ts index 90b67964..4747c411 100644 --- a/packages/core/src/types/ChainsMetadata.ts +++ b/packages/core/src/types/ChainsMetadata.ts @@ -41,9 +41,6 @@ export interface AppMetadata { gradientBackground?: string description?: string contracts?: string[] - dappradar?: string | boolean - legacy?: boolean - featured?: boolean social?: AppSocials tags?: string[] added?: number @@ -57,6 +54,7 @@ export interface AppSocials { github?: string; discord?: string; swell?: string; + dappradar?: string; } export interface AppWithTimestamp { chain: string diff --git a/src/components/ecosystem/AllApps.tsx b/src/components/ecosystem/AllApps.tsx index 3c9a16e3..f042694b 100644 --- a/src/components/ecosystem/AllApps.tsx +++ b/src/components/ecosystem/AllApps.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import React from 'react' +import React, { useMemo } from 'react' import { Grid } from '@mui/material' import { cls } from '@skalenetwork/metaport' import { type types } from '@/core' @@ -35,10 +35,10 @@ interface AllAppsProps { newApps: { chain: string; app: string; added: number }[] } -const AllApps: React.FC = ({ apps, skaleNetwork, chainsMeta, newApps }) => { - return ( - - {apps.map((app) => ( +const AllApps: React.FC = React.memo( + ({ apps, skaleNetwork, chainsMeta, newApps }) => { + const memoizedApps = useMemo(() => { + return apps.map((app) => ( = ({ apps, skaleNetwork, chainsMeta, newAp newApps={newApps} /> - ))} - - ) -} + )) + }, [apps, skaleNetwork, chainsMeta, newApps]) + + return ( + + {memoizedApps} + + ) + } +) + +AllApps.displayName = 'AllApps' export default AllApps diff --git a/src/components/ecosystem/Socials.tsx b/src/components/ecosystem/Socials.tsx index 43eec15c..f4e66405 100644 --- a/src/components/ecosystem/Socials.tsx +++ b/src/components/ecosystem/Socials.tsx @@ -23,7 +23,12 @@ import React from 'react' import { IconButton, Tooltip } from '@mui/material' -import { LanguageRounded, FavoriteBorderOutlined, WavesRounded } from '@mui/icons-material' +import { + LanguageRounded, + FavoriteBorderOutlined, + WavesRounded, + TrackChangesRounded +} from '@mui/icons-material' import { SocialIcon } from 'react-social-icons/component' import 'react-social-icons/discord' import 'react-social-icons/github' @@ -60,6 +65,16 @@ const SocialButtons: React.FC = ({ ), title: 'Website' }, + { + key: 'dappradar', + icon: ( + + ), + title: 'dAppRadar' + }, { key: 'x', network: 'x', title: 'X (Twitter)' }, { key: 'telegram', network: 'telegram', title: 'Telegram' }, { key: 'discord', network: 'discord', title: 'Discord' }, diff --git a/src/core/constants.ts b/src/core/constants.ts index d4b2e2f0..0449c784 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -86,3 +86,6 @@ export const BASE_METADATA_URL = export const MAX_APPS_DEFAULT = 12 export const OFFCHAIN_APP = '__offchain' + +export const SUBMIT_PROJECT_URL = + 'https://github.com/skalenetwork/skale-network/issues/new?assignees=dmytrotkk&labels=metadata&projects=&template=app_submission.yml&title=App+Metadata+Submission' diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 1d34f0d9..95d5257f 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -58,6 +58,7 @@ import AllApps from '../components/ecosystem/AllApps' import NewApps from '../components/ecosystem/NewApps' import FavoriteApps from '../components/ecosystem/FavoriteApps' import TrendingApps from '../components/ecosystem/TrendingApps' +import { MAX_APPS_DEFAULT, SUBMIT_PROJECT_URL } from '../core/constants' export default function Ecosystem(props: { mpc: MetaportCore @@ -72,7 +73,10 @@ export default function Ecosystem(props: { const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState(0) - const newApps = useMemo(() => getRecentApps(props.chainsMeta, 12), [props.chainsMeta]) + const newApps = useMemo( + () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), + [props.chainsMeta] + ) useEffect(() => { const initialCheckedItems = getCheckedItemsFromUrl() @@ -192,12 +196,7 @@ export default function Ecosystem(props: {
- +
-
{props.schainName !== OFFCHAIN_APP && ( )}
- -

{getChainAlias(props.chainsMeta, props.schainName, props.appName)} @@ -90,8 +87,8 @@ export default function AppCard(props: {

- -
+ +
) } diff --git a/src/components/ecosystem/CategoriesShips.tsx b/src/components/ecosystem/CategoriesShips.tsx index c6ed309f..9a56c55e 100644 --- a/src/components/ecosystem/CategoriesShips.tsx +++ b/src/components/ecosystem/CategoriesShips.tsx @@ -23,10 +23,10 @@ import React, { useMemo } from 'react' import { type types } from '@/core' -import { Chip, Box } from '@mui/material' +import { Box } from '@mui/material' import { categories } from '../../core/ecosystem/categories' -import { cls, cmn } from '@skalenetwork/metaport' +import Ship from '../Ship' interface AppCategoriesChipsProps { app: types.AppMetadata @@ -49,19 +49,12 @@ const AppCategoriesChips: React.FC = ({ app, className return Object.entries(app.categories) .flatMap(([categoryTag, subcategories]) => [ - , + , ...(Array.isArray(subcategories) ? subcategories.map((subTag) => ( - )) : []) diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index 0a996f51..81e88f7a 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -32,11 +32,11 @@ import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import CircularProgress from '@mui/material/CircularProgress' -import HubsSection from '../components/HubsSection' -import { getPrimaryCategory } from '../components/CategoryBadge' +import StarRoundedIcon from '@mui/icons-material/StarRounded' +import HubRoundedIcon from '@mui/icons-material/HubRounded' +import ChainsSection from '../components/chains/ChainsSection' import { META_TAGS } from '../core/meta' -import AppChains from '../components/AppChains' export default function Chains(props: { loadData: () => Promise @@ -82,34 +82,38 @@ export default function Chains(props: {

Chains

- Chains info, block explorers and endpoints + Connect, get block explorer links and endpoints

-
- - props.chainsMeta[schain.name] && - getPrimaryCategory(props.chainsMeta[schain.name].category) === 'Hub' - )} - chainsMeta={props.chainsMeta} - /> - - (props.chainsMeta[schain.name] && - getPrimaryCategory(props.chainsMeta[schain.name].category) === 'AppChain') || - !props.chainsMeta[schain.name] - )} - isXs={props.isXs} - /> -
+ + + props.chainsMeta[schain.name] && + props.chainsMeta[schain.name].apps && + Object.keys(props.chainsMeta[schain.name].apps!).length > 1 + )} + chainsMeta={props.chainsMeta} + metrics={props.metrics} + skaleNetwork={props.mpc.config.skaleNetwork} + size="lg" + icon={} + /> + + props.chainsMeta[schain.name] && + (!props.chainsMeta[schain.name].apps || + (!props.chainsMeta[schain.name].apps && + Object.keys(props.chainsMeta[schain.name].apps!).length === 1)) + )} + chainsMeta={props.chainsMeta} + metrics={props.metrics} + skaleNetwork={props.mpc.config.skaleNetwork} + size="md" + icon={} + /> ) diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index c10928d0..f26eaa67 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -51,6 +51,7 @@ import { getRecentApps } from '../core/ecosystem/utils' import { MAX_APPS_DEFAULT } from '../core/constants' import NewApps from '../components/ecosystem/NewApps' import Carousel from '../components/Carousel' +import Headline from '../components/Headline' export default function Start(props: { isXs: boolean @@ -74,10 +75,9 @@ export default function Start(props: { let appCards: any = [] if (props.topApps) { - appCards = props.topApps.slice(0, 11).map( - ( - topApp: types.IAppId - ) => ( + appCards = props.topApps + .slice(0, 11) + .map((topApp: types.IAppId) => ( - ) - ) + )) } return (

Welcome to SKALE

-
- -

Explore Portal

-
- + } + className={cls(cmn.mbott10, cmn.mtop20)} + /> @@ -133,10 +132,10 @@ export default function Start(props: {
- -

- New dApps on SKALE -

+ } + /> @@ -148,21 +147,21 @@ export default function Start(props: { useCarousel={true} />
- -

- Trending dApps on SKALE -

+ } + />
{appCards} - -
- -

Top Categories

-
+ } + className={cls(cmn.mbott10, cmn.mtop20, cmn.ptop20)} + /> ) From 94763935942bda8e3e759c8fb7954cefd54dbfe2 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 12 Aug 2024 18:57:33 +0100 Subject: [PATCH 19/39] Restore HubTile --- src/components/chains/HubTile.tsx | 120 ++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/components/chains/HubTile.tsx diff --git a/src/components/chains/HubTile.tsx b/src/components/chains/HubTile.tsx new file mode 100644 index 00000000..3d04104d --- /dev/null +++ b/src/components/chains/HubTile.tsx @@ -0,0 +1,120 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file HubCard.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { Tooltip } from '@mui/material' + +import { cmn, cls, getChainAlias, SkPaper, styles } from '@skalenetwork/metaport' +import { type types } from '@/core' + +import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' +import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded' + +import ChainLogo from '../ChainLogo' +import { formatNumber } from '../../core/timeHelper' + +import { MAINNET_CHAIN_LOGOS } from '../../core/constants' +import { getChainDescription, getChainShortAlias } from '../../core/chain' +import { chainBg } from '../../core/metadata' + +export default function HubTile(props: { + network: types.SkaleNetwork + metrics: types.IMetrics | null + schainName: string + isXs: boolean + chainsMeta: types.ChainsMetadataMap + bg?: boolean + showStats?: boolean +}) { + const [schainMetrics, setSchainMetrics] = useState(null) + + useEffect(() => { + if (props.metrics !== null && props.metrics.metrics[props.schainName]) { + setSchainMetrics(props.metrics.metrics[props.schainName]) + } + }, [props.metrics]) + + const chainMeta = props.chainsMeta[props.schainName] + + const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) + const alias = getChainAlias(props.network, props.schainName, undefined, true) + const chainDescription = getChainDescription(chainMeta) + + return ( + + + +
+
+ +
+

{alias}

+

+ {chainDescription.split('.', 1)[0]} +

+
+
+ {props.isXs || !props.showStats ? null : ( +
+ +

+ {schainMetrics + ? formatNumber(schainMetrics.chain_stats?.transactions_today) + : '...'} + + Daily Tx +

+
+ )} + {!props.isXs && ( +
+ +
+ )} +
+
+
+ + ) +} From 2d8b7bb34743a1b3364cc08ab7b0916134d119d2 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 13 Aug 2024 16:54:49 +0100 Subject: [PATCH 20/39] Update Chains page, add category icons, fix bugs, add search by schain name --- bun.lockb | Bin 385095 -> 377338 bytes src/App.scss | 9 ++ src/components/AppCard.tsx | 2 +- src/components/CategoryBadge.tsx | 13 +- src/components/SchainDetails.tsx | 121 ++++++++------ src/components/Ship.tsx | 2 +- src/components/chains/ChainCard.tsx | 2 +- .../chains/tabs/ChainTabsSection.tsx | 6 - src/components/chains/tabs/DeveloperInfo.tsx | 6 +- src/components/chains/tabs/Tabs.tsx | 15 +- src/components/chains/tabs/Tokens.tsx | 7 - .../chains/tabs/VerifiedContracts.tsx | 7 - src/components/ecosystem/AppCardV2.tsx | 4 +- src/components/ecosystem/CategoriesShips.tsx | 53 +++--- src/components/ecosystem/CategoryIcons.tsx | 153 ++++++++++++++++++ src/core/ecosystem/apps.ts | 10 +- src/core/ecosystem/categories.ts | 3 +- src/pages/App.tsx | 2 +- src/pages/Ecosystem.tsx | 8 +- src/styles/components.scss | 5 +- src/styles/ship.scss | 21 +++ 21 files changed, 311 insertions(+), 138 deletions(-) create mode 100644 src/components/ecosystem/CategoryIcons.tsx create mode 100644 src/styles/ship.scss diff --git a/bun.lockb b/bun.lockb index 017b32869d555e8cb6f6a6109e512b1343b125f9..8cfb8f211bcb59ac4df1861ecf7ba6b0e180b56c 100755 GIT binary patch delta 70488 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 LtO+O7L(cyX$B{p) delta 74866 zcmeFad0b83-#)z0siU)1NN6Br%n(9O8V(_Z%ux|aQ8W+8bTVX~Hx>~xX2_JxQ-Tc+CkZuv30mP46~6#QV3Lzd6j`ub&X;6t+?uyj8%P~bmi0JbdFmKQhI9v(1`Z033UN%7 zCYjaMd9~$D$3zVtLS2_c5o$sJQrD5u3E|P`^gug7C=*9W30)yy}1c9z{!+|@B|u(PC&vjEFm)DV+USvJCF=W15yKXI&!CWY$*sf z;KPCnbwctfB&Y$y z$TGq0z!w0mfdz8IHr)lG68Iz_8Ipv2;vISjf-Nu@oT5q9lXvh1I?ehKxqeJsXwX0m zdM!BB8|RAw>W~NviVtf5=hx{a2$tYMq4AD17dKIW8aD0CJ^oS2Ke#( zRX`fyQyClj3qo1>(^7$k&~C*d;Fs;N>3zbri=2mraI1uKC%AyFMm73XT?pI)Qv7 zzxs05RspAuCds@cI89X%IK}@>Aca{TklNb}B+tahV6qzv!twq*<^l!?{q+l};{aaa z8?Y1#zK~H1GzU*Z0~Ww}K#GmKFp#Ef4=44Yp?v@_(y#mrm28Kls4G#&58x$WpEHo-19uXfB5*`;5HJq0lAY;M^KA%BRNs&RAspFVW zYt&Cb0FuYHQvm9a=nH`|Isz?#u_L+b>&W~YIO!cm@eTsODbj9(({AJ{mkS%s>)C?S z)V-9sW(;rtw?7|f>9O2^U(j{b@M|QfLj4xpWj$aOxvV+R7Fao%S2zqLPizL31tv!E z5L^IGp1V7a>y5|D1A$%xy5XoPi}tBK!x5A-0sf~sHWb)BfzRzI4R^KStmzNk0p;Fn z`P>;Ax(iM*k(dyU@Dl{r$-LuiAPuPG6yDC@=otN>HUXUay9=Zk3k%X=3&Q7;oG~am z&Ji0}*r}ugZx_3xWfyY1^Ny0qd(7=dz#IGO(Ck`4EiX{~u z8XBr`ZYq?Qcz$q{exO2U=(cgP2Mh$Qhu1_eu=cPsNa+q<9>;I>CP- z&qxT1i%uLICM*Xhm$wC40udy9A4m$-JF_`-n!_54xZxq85kcgFMsmIX978tH$$$hH zL{kLarg(W1!#G`-V8H94kz}Xx<7(5TW0(jRI zd`dikP8d!xkO3qE2V`&q zIs(bS$LqMh7g(Rlt;77&MpAJDk4{q{b?AW#)IfYfQbai1{1iIzF{nrlec#Aq<{gj> zy$7Ur&H~9(aiQ_i5yNno&E)$<43L5^7)b4u2buztf*M6e2>6`eEC=BhZb)yS9SVfT z#nFy1B77^a&~}S7%|aXe8IV9dPT(_vN6|Y9sDDT}bDfW5B%YNoj}%BVum?KT`*MrpD!#KEw?Picg3u zg}q)_0f9D{g@+_%g=V@Z4|xn<0Me8ju6{Am@d;Rr!re!FKv6+a(R8eZ#s?>cM}!Dn zi?~BPfMjPKAlYFh>qatv^MvQ$0g?kx0jb;$AneyAuaJeQKr3X#^Ni#HKx(*?obL*x z25QN?43He~^D!@11f+3Y0a6DCft0^N=KtDoT#o;LTK>OdxDFQnzghm{0Uue+M;>EO z!O1i41U}UWL47D41Cs9eh4cS>BQsnL@thhxJc{24b+msO?urGdNHJiz64v_88(adU zU@r%xDVPi-7h8YjvGw)`=SpyLc@Em4;D7&I3Vnq%r9wZq4U80e14Za$Xk~CZq|AZTp|PAl59O(Y3n)hh8}77wz$wO7 z$~Z^R@dCpj&_2=+XbtoRQZTs!$znVT#)S?J9Z?6IItq^uii^XYtP(hltSpe0{rlhC z0}p^?*mWT7QzwAb&OsoR%K(z6=IW3ji^j_ZN|`G3`$-HqS>6XohP0Kj8IT&T2&@P+ z0+PY;p^?Mr4r{m;Si;3*SP2;w{zEWp(JWj$CdhIGx6g!D-5d@T;yc ztA;|*(HYdfCa>TGBtwQHgRny_z6!>pA_dJrU=?8N+Pr)#aB8?pJwEcJpa|S$6NHWq z+;gq!@R8zX9TX8166y?2L1z!7DJWk@(M9iKM^vEWx+0KVWrv1o?oU_Y%kl<1QVZM# zuB{4uQJ*i4Ny*EhF1GwOwAYVWri+FV7wi zi8EfDf9_YtYKM`V#AlI<7EU`;#s7#?Sfz@OD+i~4oK~%M*7!r^Cs;O|cJM^sYD3L; z_m8-DpoBE1uJwTMmTj&_mbM7+KL7IKoUQHdbvKz(?hMI2cYGW=)1 zkTT~N5A_I}_HFWw=g-eADQG?S{q5?{q*={S&{cOREpx zlkhF=<+=Og-=*%5y4G-T@ZOW!Y2CRtweLNx@Zx&L>662DXb%=__^dJAeQCUN^c>9= z-R{Fns%2S7TWd^_TGq7AI$YCF*?!L8JNqN+wob$hfl|B@ZHTSnmcV8veajy zrt$scZFy%eI*c5axiqYLrv^o3vhHr)64k?Od*Pi+c_Td5NjI9Dji0e&K&SnZgF~YM zKc;=Tn9{vN}%{r@n_+yXB{&HS(Dz^)d-`cg8&CbzgzcxCSN;Wgkwn)?- z?3dxBxj1vo;M~^!TaKA;TV86->$7K@oeE1*w%BvAF!27ubH8)G4f@_^-n)rY&ksqJ zwmMkr#_V0TH~ro7StiEbV|LGSKb_^iW+ zl(c-vrSQ2817_CB_Gmu4^%z<4(5w}+mMlJH z8ymUXRH*heW%|Jj3r%l+Sm3wQG`2;*cf-E5UcM=M@x-Fll@Awgxazmz*z~QFKAW5z zHXwVGUzzt4%2?Hkb`Cr;$F{R}S&I$MGwyYNAG&fueM{A}M($}=BPI-rOnZOUyuz1& zH#6>y@k(lyx-ZAeJZ*S|7RLkLSRAc>ZCXzH>e0iki+(leI6Lu})UsYx-Rmji>TapM z`P}`mgUS1^>ozp$`1sS#33W`y-WjpdDs|eLrZ;Ba`E`dRAHM=Qh5_ojwox z6eOGGdAFJ2FfwQMMBl_;-K(^bqU&4e;{3H0Zq&MA?CLdp{u5DB74}G}e|DWl+maY{QZh@xwl-lZQRO(z`>mAB|FHUmCr*L#C0TUYPgEUId0it-_3nWi_VMu2dsT;{r$xF zp--JF`F=Ozffuo)|)LRXD)j+>M^M5}akm-5$YmBZX6+YGI^%3Tl|^CF%a(qpUBKu z3hJa0y<2lf!Oj61aXAz$BmueiC zx0Ke_O??Wff2`;_T~s4AV{MJ(nSgTbxf^(?bKcf{FLP2bELsaj_4McahLL_v&ID_OdT_O;Ms)zJm6R5Zkbr|It}$K&!5c7IkD`>` zLL)Zn!w#3nZx4Bn;f1RdW3!wTbW7w!A=DZxs|G{%C#Sr|wVR`Uh0ro0}OtnmQ8 zfKk#zBMt(i!RVt#Tnk2R8qt=hR0m0^ZL!#b_#TQS&;^@#kYt;r6)!=g*oCXmW~Jai z4nltcV6+nSYexAzSPB@U71bf!@7Oos!!}?I^hIcYjRPYCG3f3Z<=PM_V60Yr1Cj4R zw3$@HeDVs^Uso``Q_#7R8Y%_ov|{x^ya@b>f%OBU>M#&J%#j&-K;99sX2`>=chV@o z43bilwPIa7$Z@eSRwE7olXo9%Jn%@{$zSwe?d8;xoh zn5%wc-HH^gYP~KFh~rC!vz=gA=5k({ctOBwlUWoP)``r{fg#>y)+j*`u-0WZ1MHuA z&%k>A$_dj`$!LUuqt!Rf8uQv~@4f0GR`-?7W5E_C$5|s|aCEHnAarAKR zQH%uV?hY=#fJoK&d^8%tnSS#WQ@}dtyGze^F$Qsw3Oc)qCQ1B&MC0xnRadYU`rUsv zQheK{gXR*IlI#lH#EK(P!eES;3`R?oA4mHrPhZ<~l%W*eg8aam8%l|D!8+<$`cxM+ z2yS(919e7I1A(L5)kwG*WN~|OruspQ6__a8)ng1t1@*EN42KtQN~|`vnEhfX7`K{A zt_I_+7xs5ijFnQOwW)di$F>rVuyWFC*WJS4cNLCCkb%3*gXJI-*V(2D5? zM!}BAch#uogLRY&u=d{~<*7G`cDL5!iy5IDKVGt(uT?%AF9pokie)D70~MFSmKyWP zV04lw1iT7^Qco)5C#oU-iGK4k69vH$Mbs!#3f@5Sx%F6<*Pp$AGzjl|vCI zRK!(no&ibUxqw8khvu0j2eeNqKa-WV=l(et}5FmXOkMZ*-W!Cjq;8x<(ZQ z){@LK--48(BQqSR+{YiFHl+p^1Swq9GY#QHhs}PO;bOkQMLml<3tJ~*t1Va?)Z&j9 zX<(hfP|#hYeh$_P?4J$ObGD&$I_}yjU|o@~lG5+Hs6ohd+C5#=bGT!%H{hrY1*5aZ zSfW}CtP>cYkOyG2`{1O&iDf^RJB8ae608yO^e$3u0K;u|mYet!DXPPFEVp@*ZKhT^ zW}Xy~sTH@)GfX!&9I+6Le1Z!DR!C)uuOmMBJ;C_or^CCmz#2;h?r!2?r1ajE3O{Mg zRrBRlg-Pu!6~^j)ZO&ws0#aTc2;pl`$(PtxXK6&+1-u=2XS7DuAIx0}%yJVmkfKrH zV8MOt5g1tr-}G`(FXU&Af~AWJjrn#^TK7uyf@@5{xEDpJ%=tjP@_;wwxin%=4i(focL3 zwYVcJ%olTO%&08>(@5Q1YtwqRXonn{BqJ)1|7sX=#1!OoM-E@D*TKk2ZnH`1pQ}QF zMsx!syKota#U%v{J15rrIi%$E*;}J3vxH8YK%Dn}NVWgd@(M7T4!)(n29tvZf#JB6 zf1d(-FK%}JV6+zXHmFvC4VKa(-BgZg`UfDS79xe`7NiQ03Z;~?)iNnH63+z7B-_(k z@zXMHtA11tPRn_alMm@*KnLg|dKxzNqDQ>R_Am2%Y!u6-)V*493vy)JuV|D@R!I4K zw94CnU0N|V-C#SGlPVvKU)CzGG}I~V>7oW9w_->b;3+Uy&f05K#;d@jg5mCJBzoy( z+>*ulVB~v#gy(|kw{HCfwe)IiH^{?og(bWVj6xByl!*5ZVE-)PCTrxe>X)|;j5@~E z5b?POOdeIHMl@afXLj*`(E`i`C9&A2xu`+f>ARzqdJwF+zPq448u7E7$IaBP<15mX zo`XaogP-uGQsH>KECHk8;ZlyZbr#H9-wK&hVf~+D8UE=8HeBByEtL~st@VudtNI)G zxr#jrB_qLF@K(S!fq8)O-amu!)k{wl^)~+L0Xyp;Fmfj^soKPsHbQxqM&${nzd))+ zA=OELlRu6W?Y-FTaM7=qDWx9Liise41R>x+(!N4sU8ZjA+;v0{L z*IkT3oTR`pZeqzTyoyTyjFke`LEk{(L>DzkFbL|yJx*KsY+zf4>k`1IHJIzJQEuNV zr5@3WCfj(3<5IX;BQ^(XgCf{^aKwxOqsi3o)nYbSS1>f@rx8`#d1H9Gfd^ZIwbJJm z!t*I$d`MV`%AD;|YOYpQD@(rubKS(5NYTXTA2gH~vZR1Kt*F_-L(D`fbknGpfptO= zeo9@46csxp+hba>Rra57#t>#^OZj`Xs%H>eNCkVbqIU{HA4)0T?v(P6X;pQ0A?Ea1 z!93;ard+a1N4 zJC%Mn`R7L#bP>A4-ji!GJ2$E-CA(USUm=oD_3z=7!TTlKGg^`DH|+62L-1gCfKLJz zikC*!0W4T5z>BL*NYTR68>@N*=Bj@QSmhvZ66?Q}M(hLDmaNggM7RV-6N>KIYE7 zG{<@DVx7cml(EMp+bdcz1EMF2;6)=0R-7<2WWV1|MU$kN-CJ=ZhQ(4oO+8-AQh&cpjM}gZQQC^VAJ##Tc0u5 zNW0ogu)b&zU9WdBK1-Kf8h9E~ef7n^A~i%`09P2}bJ&6P1@<5nAO-GrH$G2Y7mRgR zU-(n-qYIG%`dWp7F2*3dmgb@%H4~{|eFv|RqItpo0ef0pDi+=1O0WQZGc;#0m-&{) z_nkLjAv_PS$78Sj+56zaeP9$7{1*BPjKUrc#p$EDY8WxKDS`2s$6ltixF*>a;Jw>5 z-ZT%!0lioM}%nX8UI#_4a;o)@?tc{-8=ewwH8g@_G zm?Obxe?xP)A-@Ns+2D8SF8Mr<-`>xFkuUfjT+(M1j7 z&O5+xCxOusf%xgK&uaT8t7d`WRfK)4iyDN-898Yem^=+QJuL1RmJW684@OH>zc^Kk z!LZn|*W5yiJiwoa>fGfF!^9MP1f!1e%!Q+-^F5x2-3L>*6s)J-C|WIVz$iHI$b<#m zvXC?G`#E6!q_p+!Y9x3Sx?Qxo|0fdg=8xT%@*io%vk<%MTP?)#TjhZi@K~$r`2b&Q zVL3~bP3TUm40<5tKh}zAP-#YRIO1)3rH6FZ7MyZZ`ahIXpJU8w5ku#@g5gX zh|M0+Hfq=1O^iXx1vN24IPEeXN%=)u@hL>|KR@G}Jm!8y5!?gg9!sfDwW{DE9Q9J# zQ#aK*q;L^M>J?HrgPyt@KQ&~IMhY+Nk(q}Siam2TerCw*h7`|CMaoBC?8zUk(Q`b0 z>opxxJogMzyf4ca`noUNjgjDnq;m;IYI7B2w)>7|P8- zikB-eXkx98f7TM>b}U#M%(MRCR(a&3l>bpHmiWZi6z-Z>I3ZxPjBu*W!b{~(QtBtI za?lqk{}b*fU$Km&G%TEOq`dS26NF|rgSFB#dSm+rtSOlOFcxckGvv|PGYHI;*IVzR z{zGWppgf7p94ZYEr( z@-Q~k8IF|RU}kToa4`nM_1)9WNTE`&iVB5?s;x?avy2Q|N-5qTUUI$zLs9735`Rp@ zaghq<#|;L12S(cvHbp#`wlRVEJg+zpibC6orHSZlIb7 z=C1GbFjDd)U<)iK^08u+|S3QMn`<(Ki=|6dzl9 zGM=BnxSfTV#Y*_nAq6_x!|i_nSSK+3g+$y0#>Yp$WBT=n(IL{+%rF7zc!dPg&(N{> z35+|53U)SE=z|<9NvlzgH)jEAg;qQSRbB|VCMZf77)57mFm42$(6hm~fp{~byj+T< zil|=)Us;sn8NdIZ0j7W3pvp%Izb=h+Q&uj+QcI$w4qslR))1J8jY2Ru1q-tszLdyy zJj|$0gSFQ?t2VyONKWJ~82E?L`L!I3FIYO7E*EDi6H7Ui)PYFRY|F3W(nK``3I=`w zxeLbKgN{W_xj*rRRWE_bQP@?Zd|Hm>m%pBbytxsOdxf*Z7 zl-{L_kzi5!y!1nOf&{~*96f2O^N7&bQOySPlmhec>j$Jlr8GQRJL6lKv|MMAB?YkEC!=BwXi*Vc!&?64luNC z)UUQes(8#pwX-($4m-r&4t#R)b`&kn1#5=VxFX_N{kQ|OwS(XM>L~E-1iN@QaVJvb zH{|2w0->urW*3i8PIZ}WWtcjlE(-vvPSsTi{q;kwQx8$CA8HIzWFpod9Ngl}6e!iX0be`V zi!wFJ1r2ru*ehI1=!^wHa8}^GEzku>pW;YTbNmp2tz_&7RD$~e@ki(^^R7hjA*B57 zGAE?=ePvEq0=y5thfCl16$Z%}gj6w1#vwp5FiOTaAeA2@=j(v<`FBVikC)5+SJdh2 z|H}eo$V616x+yYFl5sML_z+SD(`1|}>;DRA#1itU-34;F1!Os8ERh95YH*p%35llz z$rEdU)WA9!*UR~vfcPV1(hnJ_-d0)PChLTh+>RgAp50D4gOCi_4^#tlsQ`nPO!{LoSNr{r3Mlsqlt85z$4soZ(Wl#$H2BvB0l74+LOCoBp67)aK= zpopP{U&#fEBaP=Bbjo@!=Mz##pMaGA4MniL24$GpPE?6!|NEJh6UL47wL9$*P$&ev(y+}F#-yzNYaFnB% z7|qM;42k~{k|AT{j*250o(!Gx$I1DG#K+4xLDmUr`+u2@r^+Jj!6m1%ft^F!B!v@*e2^) zGG@#9yMgo}q~u=wpz`}u~<0Ld_CAO*Rrj4goFPD>zt2#I?F$sjLT|F4Mop@MDY0)*6Idzlkb zzPF4WaeXBQ+c?>;Dc*quf}z93jQTL?AVsBI6`EpU@2Y zJRr3{pX)k9;y)lYut2UzNPMBp{}oa`lk*9QFOoSS@x?Nx0x2tve$d|;qXZRLM#u_$ zuqcS8>+?AhUkOfzuLjagte5i%DSv~E8)e)i=Mz%B%`zvXqi>JQ2~EHc18IhGX;IJz z3B94mfz;s{Aa!&WNFPF~a8c%jlz$0G9TdoVaish^vi{#A@Bcp(q>6V@k@meOK-w54j*A70?1{m9>%e;z;>k&}o%*15!}=$>j*iBY{BLi3iI%AsIXbNF$HX z>v(BE32HD>FK{Ft1#S);1*8U(<#OYI_#>p?2Q@UAbmr4kQClBOGbj%qLK;^ZkosK# zq~a@q)ch(LR|CoZjX?VRJES68@Pk5g7m(W7E!P9`U?zb&+$U!gM>61mtP@hX!$4{% zN5&(vo(CicUj*Wha1}qO+%+Qq645^uxGqaZe^8ngqF>uLgNRX31%GdbS{NadT$ z=qjTdkV44|NcDYy^dThf3nWPwAPd37@FAp*Lu5`!$#DE2*Cqlff4GcEl!;Gqqz=Z( zdU2$B6J%XR-G2}$Zwh{pC#C~wWHV%(NtyT%($Th5*3)GD-yyZLOfE-A?WgNGi_$1c z&@nHpmh%az{2G}P(xTo7r2TKFtP@hbJu)wj^K6^zD{sLfk>%{DI7$$oL#c zhQ0vekMIsZ=&cK#l;kNDkPb~V{Giyf22y*KOfdg6vdWbE-yk(y1^I+kWwZxU!?l3a zP+b`t0Lg%+Kq~JHqz*l0-b%*SK>D;cf&Zys8#%*E&LE_Qy@6z4M>)R}klvp3lkXN2<3`)(MGk2GYQG z0I9!hAO@xr_R7LOAbkj_qeDQdctqC!zaSZwE7v0=ehf$##LGa6{#*L|iiQM7a`ioA zkcEZ2Vzmm>zpvKe3Vi6QE6SL?K=|9!Qtf5%S8&fiz- z{4}-0Sx)|_^7qyH-&gDMJNCb?*0Ct)uKV}Z`rlXUe_yR*CD1e3zk8*QY<)g0n!m5s z=@mOYBL980{`b{7LgDYLb=n^Oyh;E2YTb!mxZy)cJLcb4>wjOZ7k|}GZ#k>TZ#%2X z`1jR1y;7$SA$cenNKdZgfq!4ElPCTcUakB0QB-QhQk2S$${c}RQYw2f>tzaa=4=Gz zsKCY>K{-LnnpKL9iaeH+rs&N^EGOZOqNDP-z`DOv^kNPxps3zKIVrG}Z=u{E#kVJ# zIW4evZ=g&`SD5?!ez+pt`t-)^N4k~^cW--kBim-gp*PPLS2<&J?BT_qYfg@vbbj65 z1k2@98V*;RbxL`%>)I#NpWR0sVAJc_SXNVhL?kGLU!{Km&kC#q_2jY=JuOIwdFR>N zRWPrJ1mD##=Ayt(DKLUXtDrd2C@%}_*-cbxzZ!~9KFVAbSZY3$FQoh=nOJEyrLs74TVqO5HKwtp{PSO%2HJ5UM* zwwDy^^-%2YLU|ytA$OsiAmuD6j|66W56XxQP{!PY@&uzG#bF~9$3iGi1vat}$_-Kq zNO>->y7!?>*#u?geJC#lmQRXHCKQhcP+kjc+5;#>q`V~Mt-xF#LRqvKO4>sx@8KU( z+HZm4^9agE_~#LnFQoh=g`a>i=h03 ze~O^&CglJrzu}*!P{OjHL_Wm?Dp<}lOrZ4+2xFc@Fk*FHLO4M};uQ!gmUD$7FdIV6 zs}M|B>{SR3J0av2PY`2!%rDdPLPtA0i~vb9U*1JVJfp7=GA5= z_0G+K;CKx}9cDr9zd^#Ior;b=_)9f^KE@q#PYpX!VaYS2PF6E5e;hw<`*j4nxu}co zNnKst*IBi$TPmGQqhqe`d9>O*Y2v3U4Ju7Kd1lhrBic}p+wV3v{g%SU98p-aM;l>_ zqk=Ww1Y2B=qNfF$pfpsl`=k_+;*|--Nx|l2LRpjxpuDYrpUpMY|J=EXz7N^Xa;o0LfM zO$+9<3$6$|31Pu52%fAEf}#~`z8lb*%_V5Vo)UO5&pm*)Y%xJQ_LiVM^WF>aX6XbS z*jIv%tlK_7C$^rzhbi`>6PI)7By>MI>B6=_P;_PD0YEnv1YnEKBRlT^vU@O#gAm$Z zfRK0)f-lP<;R^{h4?*b7Vh`a*#zhEMN$_X(havb~f{=0;LLYXK1odSI&N&bQ*|;1C zyGeLNLVxCT1VY#q2n&ut2x5gKSYL(UbrixtHuorm6C`{jA(VOMLKtxk!kSzNgV|dW z9Iiv~&4Uom((@qPAVGBu!cf-j7=$S|AY_ve$rQ&SxZH#gdK^MD+eShW3FS{fh-E=1 zAS}v|$%=#WW@j8c2Ca}5Zpig8^2~wEnd1Q|$ME08V$ezsJ zlHhP3g6{y4y`QZ`>NlOa?i5VyZ4zKj*cl` zZ?(&Zw#@M%)|u)ex}ME!cA)E`hv+Dfgt;s!8^WST)N?ijiLJi~q5WeB=9eHWVE&gN zd?Dcg35k!s4Co19a5<&rO zSL<0FDsh7ZAKX`bHWuHmn2q~URmqo&whU0&Je&LLVmY1$S z>U60G6W%oSZEW0R!W9hc@UY@t@bA8_3cE3}-;>4QSlS-X@e(b$ay6I7?uYD7%=<3}o zV`G1>YD&}kmGd1ZeA_WQaboj{hi0nFCJtS8mleRgPF*uARw`9E`MSm3t7Gqr!+zY% zDdB2W-KK$`(UI_3RxdvtzjrAA%#?l8d}ozxQZDG5_xPRDyOvzt)^Xg1-DfOw_1=&Zug?Bzvr;*6fJLGm{#x#zNy#p5-dy&(UPha0*^{fUdFJ9dc^Lo{C6EiE#r$GsMq3c(6Eg@*U+?rPz5<0!KO)3A=$6Kx$ zl;}PuVe6NsPaZgioElZkyj?iQ$KVE@@eWHcdjM|WdvNa<2*vL`l*EBh_Tk<^iuwZ- z^KwuQU^fVXvYQmIFery`)`vj}`-s_n@j4~KA$?QjUN+nOMjm%O5^^g1)kfE9O_q2B z$L(5J`=zbX_LF0urI$U`tjo>YYjPsb(ST-tOE;B!mk@FO_oSY_-AZL1o!e)gwwU{lDp>w#4Ew|Bw>&0oJ= z+i&cUR$N+1vRl2XzV@AMM5LzF@8FXwv?CWU5J{3Ay`xtGr&2rg?v?_nmrkmTZ_3-eH6H zc#mEaK9`BC=VRn?cJGQ!k3WuG+bE*mj1jBvwp-vgD)X4dlw!lC3$fv2ym7(#Q~4=n z#!sFzIJnWY8`r(JI97bps&cd6g&8Hf^(r_pz3;YW$IFJ>g!CTtc}C?66*77!#MHLm zFkxxgm(OB5|Fk*8&ceJ-;~q5}*7^3=qO3+&jpo|+x~_~F_kLyb!dtoJbQ7~rU0wa{ zTxfT<_Afk7yN%V(s94%#M#I}P2A__(ym7}dN89!L`Cr-Pv3Qz^zp;T$`HG;A{Eg%5 zEZakZ%QpxI-a|Og#19aPNQnFZ;Ue2Z!lLh370Vy?D;m1LZ^Uvd$KYKp8%21;UdU^* zeSZB5+MZLsJCE*hWXqc597%g%yL~^aaTWFsSb1vZ$9>JU>pcrgW%u~dm^Ko_PEUWK z*zhqPJ6-$PCSz#VX2G{AeSGYo>-YS9*MSMr{FuR22aPTH>r$G!#;vbg8r$sX7_;zY z*t*nBeY-wN4&RjRWLjZsZv6Qn7R=)Z;$zGw*n5?Qe}uhXeqaD+KSH?9Y(7EA_zA%d zg3rz3UDvy^=>6-rjjVS!nR~pGCU)7}x@A09MdZg<3fOYo;Y?`Jk7|iJlY6Vnm!0u6 zdh@|;-mju@)L>%3(D>fZauu1FY`FpmAC zcC&wC00qqHD+aLpHwLiaD}=i&R-x>@AWUh#z)smwQMe#niCc^U^epHDW`kUI0$g{x zxyhwxRiF0xeW3K%&E=-$J*k}$YGXI`_JqCjUVUp)Q0m*)M%Av`uXI`8EVaj$y}7lm zTC|Q&ZuR4Gjigc8SEp33Rcyi@6(3!Rstv^SC(+g>+nYrGp4{JQ%4_$gG2ilQE-ba# z|EmA$Z&f`bC)Ls>+BlleO!DhrHQd|2^H(>Y8=?0rT{>V=wC&@57O7NP>z01rYP9~q zV!MK8ov+PZZvNi2hyUkw<;FbwG^d%iQ#z^1T&?x(ZFH&|C zu21Up(`N17p*J$F4E?@oqqR!B+hd<@`^4DNA3_IBWp+j|Z}7S%XZG&icl^9t{ZT%h zS;4Is%BJ#Dc5>`%YKm%}T@6t-EK+y7dxc$IIB@^vf5-d5kN?Gkv=V)3BfeQh#7 zOpNSP!T;5?repTpUG^nzv)A{M(HEk_YqPUt-j{3jZ<@^L({PeskBLie4_=;rq*PkX zmqP1T;iJ!F*Q)(`cx%b)LVl&Pd)~}#Zq>6#aDUz1)eTa|G&D*%bnpC*Q6r0)_q@1y z?N)|HS3CP8zWThEpFWPX=n`>K=)5@jp}VdiDt2-wwYK)00V$u_cDKpQVXNcH-PcT; zy}U`c#g+4yFHJPD@3H;e3pU0W=2;eP+mFX$giFl-hti<@`!{E!J* zC*BKo2gdG*IDG!=wy@H3UQdb+YhabKPTD;*M0GKDobBP>Huu*K3cq~jK){;%b3Ef3 zF3bp;C49ea{-Ap==BR>s?hmaFvX0O1j$in={xpB@mo@F{{>Th=KCnOfSlz{Uzgzz3 z7G_=M{Kc>{$16|1C2YN4XXp8=(d8_ruCNXK8PNJB{d)`s^IjJ>FQm?Zq7sV^tlu)g z)BR?_iIWAdy{lDk{mgEBGwE!_S$EF8Tv6Mr&Y2>c=&&7U97fgXQX^o@s8eq$qU#8A6k{Zea88`v+?xDFAvq-*M$CE27`HTi<@U#H%^;(?&|vYLtB;#y}kFt z)O{0!S~abFW7E_>tll9oqnCmXGEE1_uB^xU+cX7k2(YSFs!(eM?k z8|TEaBa6$vJrS@drQwm$h2|S4PE9Fhp5b49pwqy((wt+*SwPikjZQ9pkzTJqG!agyWrF7w-;Y4dH$uTc#j)$lGHT=B-f~w_5hhta|=*_5xmD6`oY)2G*mbbK{D*X7&J!WAiNf=XN|&(hQ|uSMp~;qQ9d zZkatS%X#6?3Ww&83%b|rOZB-2_Fj(~QE%#`rn4Jg@&91vY1VmClbMz+-pr3r&VAzd zqL%C9-DMJ8p3ymI@ZQ(r<{epR);%cd(a)wGA}h@v(tp~wK|_@{FI-AHSi|M$?UPAW z#9o)zyIDN8aaU(qg8JvR$m zI&T@7Y$G1M-_ug#IiXh8(7C5~eBF1_>EX;u`@bwbdm?s}@vt2cS7)`HHhjrK{NEt* zV(yo0Sj@cd#m%d-z2~#4?R?fJj$bx1Y=iC4-#2<#*b0xToxJtD_qX;9R+!_2TqK9?-5tidGCU(mSr+)Wwwvnl5nlu`s?m@ZkCxoc=G7}qxbB;P`{XY z^pBkwKE`kNyEx76HS?Cy%~g(Wt+QNeY1YIp(%7Zl=zZMFx8%I4v(3X!?S2|$(E(U9P$PxG^S*>@L5~ zH+}!MfqCtnHQcW(t*gs5tvP*c=gnVTUv{``ai&3D>Y|Y5%Z);M*D~{^e{RMwxrTpx zg-kSFH{{T+ujvOLtoqq*`htFYo~KXvWtwp$;KRhxt>(|H@G0ty|5Vc}f%Y~HX75nE zi1?5&yX2Kb?}VnEJkB)Qa6L5R)>IZ~3iB>58JiXN-S=mo>8i!It|hmv>DRFG{YK%r z-TTMh9e&H?%jcO@ocF!o>9}{=7n^K#)0t*#B5QxDo-^xmw>#bE-oH?MauxI&Ecs*n zFzC?3Nf%npIZ$D-g>CMZkXc=W9*#fPz1^@mU6)p|0oK{zDG8pE`<}n<=e*D)jqsV0whm>$hp=***;eLu}oqRoML1?U31p z)jwWLPI7N4eXLKJ*>+><#|+%T;!oNKYJ>HSV6TU{_3SJ5olsX>*))9l0n_0FUn_0P9|dt}>) z9?eF3hvZrhQ*Mm7VtM55f#;QqwX0IHXDjh$Z-luLucTJt&6kOi%_GI36cn$OP(=LW zr0Gy@kTRwkZjGkwWHsCxr<8_}XbJ)UI;klHmol)fY3YHAF=wZnTV8k|w)T55d`QR5<+SG~OTg!2AD%mt+DC+W1UXo&`WUeYGyGcn?L8$_7krGw`icbkB z_DYso0*bX2l%J$jSF#Q!P)?ANX#(feWGBtwoDtR#9L*urW=ZA{9Bd#IkWh!!DFxvM z2{TJUsK;)SFvS*vM`;L-Y-(u;E)^lXB%vW|UIs!D329{@II*WBEUE;-#{xnVw%7tf zdpig}NpNP~Wg&baA+sz54f{$$Mr8m zpz;uQlaNP33uaLPLYO^-#0n5RSq=%-)gaWgg3y}9T0uBL!c`Kyn7uWG5!E52SVL&X zE|TC-1A?;+1aCIZ2Eq*z9+A+IIoU#(QWL@gTL?a^kOY@n5WFfv=)&e!giu7nM-sX* z8}j<1+7QN=LFmCeD?w=Q0AWof2)^ts313L?rDvnwEZq)5Mjd4-R-&@91AZ6Ltuh3^ zx+sxd872BKMHL9@dJsaZKnP^pNZ3t6`Kl25v!JRF!s(gpg$K1g}~UqS@S95Q<3nNJ1?0tPNpNV+d<%Lx^W@Nod~$g0BOFM3(LV;R^|>IuM4l zZgn7JG=-2&LK0Kdh2ZB5A+#=pQEVFt>Shqi*Ml&I1=WMFn}j?Pbj+eYgfI<+#QG4% zu^bYtT_DtSgfM}{Izl)>!c`Jdn0*5XBbq}$m4I$hh;SmYb znNuSOQ`{gdXar#B*CQ}1TS|8Ti9H82t_1(Bw-u#^nkFa zJ%lwL5VF`?654x1@YO=dX82!3!XG54T0q#vy0w6i(E&m>3455LB?P~Y5JFo**vGb! zpzZ{ryeEVMEXY&YTXB%>AvnY=S^*BTaDp6`LvV!Iv<4hyu>`s7BtagtZv!~Sk_e8o ziv%ZF9WTI1HjdyFyGd}GIkg3xVN(gtvOZgx^)2DVCxBPGMtOO75U7c;1=6PaGQyp00k_F;11hE zaF<#50PeAHf($oID%*FCc$&& z)D7^0O(l5A3JG4Z=G_6W*<6A*>?y%p=Gg=Ajx8p5&)x!9`~DbWW={<9Bm3GD`X|=S z7x0;_C-}k?y#QaCKfyP)jo>>Idjo#3AcCK455X^H;RpE5!U0Obh#m1$_EO?Y61M(8 zr4frGG%{kR2#t+cwE&>Xh>avHVZ<&Gni#RVeSm5sHl9#4V)=w6jacKpKvN?&4alYp zhS@Ls;)pdjVy=NuT<}thr3FGMZN#3DQbdYRKPVPPEVUn$Md46>l45DZI`oIqeh8Gz z{!q#rv2Ub&AtjK08DM3^ltEB3hC&Gqg7W{gb{=3=9PQhObN1K^s371$!QMcMf>;oZ zy`aY4K}1Cbq}dhhU9mpK#Fl7cVoy|TF_zd%)L5gjq}ZZHqp>FX-S_S+5F_vV{;%uf z^5@Jx^GvThyF16-Dqm94c@QKny&)-KmEOG}$rB670Z8!HL_!~w>oy9`^?{(2ct=Cf zKMsP4(GYmcX$supA*j(8f-*9;F9e4v_?3ck;@=N~VS^!9&<}zNa)W}32@w2=M-Esd z)aTaSMG98MKu|>9@R1b*AgGxHfo&iJ)uin}2yRobg#v$Z90b9lWC)@M zK@cdLC}^4jflDj|H6$_?g69+*pdeVB;~-c)1cH<}2ts5p1)YaNP%a*V+L90tL7rg{ zoTH$wcn^kP8wC>wLr_mnQ-Gf|NA?pSs4rs^AaEN2!LJm&C;o{L9HwAFA_R@(1_i@L zLeMA)f+jL234)5FAo!DlW>P;Hf{PTaN`|0?JfdLAXb3u_K+sB7q(D$}3u3FmWUVz2r0n{U<$^GpcNQ7}@xCqa;B76cO~K`>e>O@?3_1v4i@ zkSdob=sz2Rx>Fz+D^sUH;Fb=-eG0})$W#apQ?O(z1QX>B1;ge*&}te4ljVbH5LBEC z!QT{2m1ff+xJbde=@3kp7ZgmH2SJY+5X_X%XFyPMJ_H44LNHsp%!J@J1z%GzNAk{s zV9^2yVrM}xPqtIgbRh(uvmsa@F|#3fPQeKZMBLLMSS=8YPKN+*A5hSF5d=PSAox&* z&w(J%2N3*70p32Ci*a~Y2tK<%iPo@3> z7@x@pG*-(a8lOwEg)qL56*Sh!3mR*st-x3(pVL?`j*DPykS;Vf$|f3{GV*?4dF+HA zn$YqkBu2&eiN;wAVHx3DE&Fn1tXyNMYMGaQQ_ zWmMg0(Hz}p57JMKNsAg3<2xY9cu)2rWO*|-ePDUxI4`1BMMhBY!C-jAtF-5b;CI_H z+HSSfvr#U?c3aBIsWp~1j4NaLZp#cuULMvRoxZl@srcan{n&;y-n9>i9G(=9)2AoOJ|#9#!vG4q-BWB`4^S_;(F-wzq6#dm%FK-b)u&) zI;nR`%%DCF{UFQFP<9$Io(VR;3)pJQhGdMgXf9yuZ=_^h}5=9oiARy=ngn^TFH1+ zM_DCnr-EQK<&>8MS$^N*ATa<6s5={;9JqQ}uH}+NjsOdB7#p z&I4@TXNvI0LoO-f>)-8^tcQ~Eth4S))>FxtzfdLXrDQxAz>*99;V(kTc;J#X7rvKg zLGk2FMp6uzCvEjsGM36`C^;4$Pt@e6K4dio_#3YDcrx%P28O>8O6CT;kCKg4GIz+T zE7>UXj8G2~uq&B)rfFTsctj-s%=1n0Ybg%NJYbwT;vuJuuO8^8B3`FtVUTrKG9G?PYkkl| z$u=li1IT(Q*+wOM4>Bf@iQlAT4Pj>jDf^Q3Pd|+SPv>NgSpSqY2F|EE{%uyWCb092 z9j1PZk~M|B9PIqts$|VzucBlbO4b}QA0^wSWG&eK{FIP&$cS44eTr4v^(iGJGhnC#EB0d6evcl68VCuaX@!PnqZpVSWf3z~p(g_~+0CY-5)2 zcT{=q3j0naJEmmaAY;|N2h%*`wma-R@`%-SLh1E@or5j^PQrv2{FDsxKS~LC@GTS8 z3q&i~S(uD30&ptf-#I0VguN~7EnuF9N$1`mh!*@^g30irKnU!t!pku6k3T@?P@EQ) z|5aERNi_J57W`dPvc9nY%uw)mUCH{v{)>{`P%>M&ePmtPh^Mk0 z0*Ap7a1b$lw%=4%DQL2JOp8y9QsK?l$o zbOBuf7i>Ww7}Nx{0T*n4VmyDsxfh3;aPK;DU_nuOJW%xbEUQ3+s9ZEan|5fy$t& z6kKL)(cvu8dk$(EplfJo2;Gz1Mmex$encnX~t;3aqk{sMo4*WeBK z6>!z%1e`$;z=LFW*l>F9E?9PhZ^ZixYlXHaU_A-G1E;`ga0Yx2&VqB`Jh%WZf=hsh z(C!2L9{e$i<|%jvc+&Gs*}D=e)VZ+E1M|TGun-7X1U>+Z!G~Z9mBf=-|d=nA@l2oMQ+gDB7k zM1#JdALtKaa$!*7v8)A=9xKxI8SK@-OH>zsvtk*la5k6&=7RZP0ayqGECL^Z#o$9Q z4e%6cJK*`)`@uS}3VaGygU`WIunhF$F{~V1^}!SZj&~f(II442_#SX_=45;ka031T zaMHa3_`^o=U@+hsn(HmDuec`W`l&r+T(YhL9bxAZ*clW7MZs$h$~-~ybMPtPA(*?s z*I+le3ip#>KiB|Pfn|V)-tGW9!7Pvnl0km>c?I{$C;^@p-3gQeUceia24z55a0nSq zJ&ga3fTQ3TI1WyLQ(zPL5_|mkN^@vIA{&pfVO}KgO>+SkZzuK&Xdi51-HN*a2lKe--EM&r}aB&4&@z+z%mu- z;gTi|j0R}-j{;)= zPsKQJFYGCno^ctNh&;u2H!rrl@TX!9f9%sxSt8oSe0T-!Ulx_k8!5}aexhsy`aJ{(%d;}H&t~IBEX<#~-0cL^O zARWAd-*GU+Pd-9Zn~6GVVWPzh88!@(S|04xLo7J(1IV(=mO2rL0h!7{KM z@JE?OgE4@$-XBZ=6Ilq8V3`c2fT>^_m=4B*G%x`0*Pq4#ZW^`$Z9z8>0$!krSTWjY zfZJ|77{3~t47W5wKrK)n)Ii|`fpqvejcXq7+@EJkbBn1L$Oli{hTzHCI}r#Ey1xvr zfFHqCa1C4sH^5KeXYdR772E{3z-@2`+y%dZd*D8J0DcF5B95tuD;6~2>Z=~Og=)Jd zkzZg~;_eZ5i?)NsfC(jGP%$&n4myA1nl}Yp-1D|+C+L6-a({-~9VGy_ICj8qeYm{` zxW4}pCiYzPTjE2o^FE#T`Mn{7ztl{Ryr1?2g8;WXDnZE$aJPZ?>85)rxHp50_uBC= zBS06x+u}B$CI|sVzzgX72~L93U^Buw26H3KqcD%~UWmuj76wj$cci=nWh8v(crpUv zUFA$L2aEw@!9WlN-UsbKd%zpW7N8cW4MIUFa0cOgiLf^ScW?*Szk##hTTUDYz(HPQ zvoD870SiH6U=}IuO_Ypg0bl{VZ>tR+A&iw^F<1?jfcb#cK|X=br?_5DIqXIH;=h?7 zeIH!$=k}9+560Vahvi@s#2Wz@q29PogP96S!_Luu0EhujpfD%|IDJinJEybn0jD>{ z(-yp+@#|V^u$KBh?9obA9%ebfn&4E%Da{rB4}t?=4?wUE5IcMe_5$_z;mv;7_ksL? zI-C-70UJ1jYwCXos84r>iS<&d!`HCv0w=&Ra0DC%hkzNlX&!})8KVAiVEUzOdw_RxcfomZ8(aiG zgP*_+a2;F&SHNZP1GoS#0qSz4_#-F_enI>WVc=JA6WjvyNXI*1K(=cRArArXk$AuK z2Y3J|`yJ5z2{7FrDLJpR+v$(`yx&SSl_@YC%mDv_jBC^vfGhpK0cE_V{S9zHQL=7X zr^|sCu1f(db@WwYL6{a$1ULa+7XbM|UM`37z+wftK`vke8lXbHY!rgMu)21J$!pWS zn6h)FPP-YY9SPzxl~u>}SN23OQ3W{esFY+Tgi4IS4oU!bz=&ER5D%E9yQh+ugvp#S zSJX4@-s-wE%rby&tQ^d;fWv}-bdEa<09O_>0Rx#1Due6^tAy(cpdxs?q-H=i4X{M& zfhyp0B(^F{Cddzjg1UfHNp%ni0zj%S{__XbfDhm`Jp=t#>Km!0(8M@h8DV3h-kshc($7GNQT7gK=67&RJ zKm=gGocP* zbOH>B3F!=&VESbS`oZi2dV?qs4f=velu`oB!5|*Qfk9vZh+!k_56eIh3z9%EJj?~- zvf0PNo(9-v#(>da6c`CcfZ<>m7z&1f6u_#Ok8l=&$*?orNiZjZ31A+sr!YVYc`*a# zbijr~%q~|*UC)D=4rYTn%A5;xzPethWHc8k`v)*h`4ZUa?;}9@hai=K@gjSm=0L)T zoxt}<+;W(e!7@-Du$8~9R|RrY&(v7~88bEwFf8gYlMD+wsd_+kECtL6SHoTpPW0K2fN|(&6^Fde+3vG!{Kar8s@Ly7w|K<4z98OFM;pD8E_ID z1FqmOI0O!Y17Hi_cwJOK9r1A7cuzvLBQNik7O(BI$<;5yCRv0x(e;F?P^ zPS?5V29s??e-KUq6}ZIDfCKN%dE3f6Qm&SH%gkw%t8=c>Ip4#`TpJe0MKQo4E(+5b z?1i0rMU;HGx~5Kcd#WoGxN~3%H7HPt0h%%w_t^Tq zHjAISH2q<=0aZX{z*}1`c9}T1<0r0g;R~`WRK#@!rIZJXRAvHv0Pl+#2qWjUpSq@> zYM>=x#HK#OtpTco9*}eMAplUf0OHqg_JeQ{0=NOd`v`WK7BJJ`p$^Q&FnMoLA8l{|y&Tu*b)4%CnP{qOUQ_Tpt4Z#2zFt;Y&4wQkh znoNDu-Apheph}UY0V6Wq8HcF@YpTBI!9Z}~&>R0nf+#>G24Z^d0XrjL?iq-g z0P2|~2eH2I5(i5x=nwh>5i{Imkd)0%naK#)hjVv^sTu}`0(zVbCV^3a0h`woVNV0} z!<`-O@Gvq4I3A1xV-*u%G7P$zI?E#y?S6~BR2{1ulf=yrrNC#|)8v)(fGm!Aq%+}3- z=$W}OTRa2#2AFQ!Vc!b2fB}G|LmdXZ4P>ZmHbB~U16J=YuoLV6Un{#259*((F(Y7Z z_k(?ab`vvEjO;i#23WL50dvos9|4EKLBOuboE`!UgZ@tf4obAsFXK7G`Y(v#FF$w! z`y4cuqu?y;KY|}XKfn>pTwI>VwP~k$4%e5!MQ{O3h3^o04{6H_@_^hR7vQe92CRUaZ;pVI*Bf|y4gLmyfmh%qcmbY+XW%LL z6L8}$7T!uLI^()O=m+`&J|OACca^xe*#Sy?GU5x9TaDau;-;+;EoB;!L zhv^N<0XM)$QpErl$Xp1Qf!zy~1l%X3KJ}X->-O& z5DfURss^YI0zhR@9+(N{_464Og~T!_yQyVzpD)kmFbDl15J9O z-3*kuVqzKSyCOB+-{prgmN5O(&bGsnbZ7l%U1T*X!_n}>;-V)raV&BsxUQ1ZKd)JO z)FWm@)TKLROaQNGr;Lra6ziXL(-bC4f{{>(CL5*ck#(1oo$V%jpk_p@ch)oAXb(ZR ztO|NXuxMjJUDyZ1WB?ODqSQZv_jy<|V*qQX9~cGtfYD$ipmzq#Z7K%c4)zg%wa9@r z1u&Z7Fo%JmV2Coa=ab=Dkt~`Fk4c1kYGxD4p3x~#_ySA@Rx+Oe)1Q{c14m)$gfIfryDOe7c z0w!`9SOr#sPrwTBG2kRlJu`B%L|E$|femn53nsxXZH`&X+ScQ09Zu4)qJ-O)X8-tW&w>uYV2SAlZ4x zTEeLroQuMFZ&cy*uj>Xcm7900WlL-g!G;B-_4l+|6hGLvA3XW{`{JM90hxK%>R#e5 z6pBIN@6fAlDtW^ zF=HZx{W9{N)g505`_OnTkuaprL#qeXcl~a4b1Pj(-!n*CQuKO((66>Wv^e_vR`>M} zl(Yv{yR5%w?JdhoYF_?5p@;p0w3MUWpL?zzmXCUYz5yta0m@~}?MqGD9vb3GmjGXX ziM(%hkv1i@JTm6K)fFF;Zn$rClTsx$n~Zy4^;ifbt&rv+UzE~3ij9cxqnEEw(9gd$ zX_&gVm}CCB^1OoPB~MCf9yrVR^aHD_lq;nLE4vkO>sfIh9comyXp3K<2}Crk!AX>OJlVKU80 z>uYzYk9|pKHk&ZyX`6AoPqIxRb;xuYIRUy_}*fS~uG$=0_aX3e?dMR%jR z^n5@w5-!e+?Z>u=>E6$pmeKIuI2nzK4$m?d+j-|57U|xN3DuVPZ zt1p#{XaSZr^`$4SgPy}jUig@7n_Ka6?aPaE(G)R~JSncVv_!U& zs_vTGfXS_}IzyP9y*?^4bbInkJxo8}APnGevBTy4{4-;IC_nEKTvQj@2bU6Xi3w|X zA-1q{*DRMGl}qfRh0~6g>as7(<#{W)>V`b^4wo?=Fo%aPETp-Vs*XFv9Q`Z^7r)2o z_UpoB=_{B=aZSIs!zBV~cb|?7v4Q8`c4l0S`1D}J7KFjjw+4e0s5 z-e|g^qYJwJoS4zt;)tQjw+4#zIaW>uk&H_}g!lisNTu;e275F5M$TDN#j)_LwYc=~ z!mv8fQ}eWs#SJ1G;pqXLF0|~vWRMCe5UacYL$R4f{a=dDo@4Q4#->We?Gn=Efpk5- zQs!Wzm$yfl=@JjS79lbda^y;Xx@!3jPwO~6ZZwkshdZ66`V(tQd$lgOH-*N^%2%hS zG^^BH_pjIAn=aCZ4bP*iq`kDd1y#aU7{}ae>o>%;EAZtFXtJg;x7AWENl(iz`owQm zNlvT!fbP(utDJ;iOZTqgR}f{Ra$-sDD(eg2dU98(`Ul+KYUFOW!;JwksYKn%spHOJ zu~Gw5xSxYGeQC8@KI|$H^!*Eb!<2pbweZZA{$)1_{To3Z>L$CN!epaH)b?xLj7jnD z+?$?{=~g*NPrg27{M}9dVlZb|l{3)X^@6rf>TYzfOLaOd7e%-rsBEiKfD>P6Zth@Ey#pyZ9>;N=4-v&Nt-#1sofgeDF^F1^!!G&|%g$pb9 zcUoJ>JuCFv-K8h|T3&aT>oD#4@XUk#;^n>9aZlImJpxU={b1yv>LtZqU&R_k!k_fmF5@tiv36Dtf_jxLk`~a1*oG<7n}4#3JbL zXc^~->jHgct0R24_s!5OT9BjVct5FQg+t-~GS~`-kQmu*g@*<)QeM+;tDplcF9%3N z8{{ts%32#bfO(ifqd9$c4o?c!RDB`i4+qJ08z#=^SfidQKDzpQQt^iw&_Kbkp5kN0 zHy74OqoBe1Iv?Knv)^Xls16NuOBUWlxNthcPal+T9)9#VT-1a)Cssz~LVB0P%0`4? zSsg1!b0OWagT*a3%m;&|FHHN3!A9Y_-G1%cVs^?wgb-|mojXBJ=0@z^3C5(n<=5Mt ze}28@wT6iuHvpK0>cWMqPp@wuS}*!HYMbTKF+uEkkoyE^a1$@xOJjP1 zw1Gy@7tr9U`Ec2*tG9i6bWE1UVYqn1rCR$RLr!0AP(RD%PJ%3@-@J*&LSp2{OPUYs zbSaxgMY!bVNKWbjHw@Woz&hDP8{ew@>#KQWmdo_L1 z;d<1J@;IkSRhMd=Xv4^(<|xZs5m?kMpB{|8WSUVL%L=Xf{ltl&qgk<;%|}g&_TiXc z?WmLSMXOEQeYksRW5!0-G9!9>hRz;Q=G=-RGSkaBwoT5#CLn2L5#i_^OLwgJs}SApX@HFxmrW8N}O6!3vT&;-Y`Ls+r}F+!6(NS4PQ3kObmjqi9)L( zbxLY>m%VT;h!PEX)wN;0g{`rcM|q0l9c<~8EUD#^*A>)eO6Jyz??|+AE@bCa>pHXi zmbxJp$I47P{)%AJf02|snyHOX1{l4Gl&%!j*T<6^N5f}$wgK~ zwPBCYrz;2i3WvTxsMTop{8hE=kM*Jn=43YmBO)a6gg|DB+%(y=D#H5Ql^$CJ$?!VdMM8E_ux5E7IZT8|aWYjAt34sa7p& zo90(%iSB|Mdu&pTz};Bj5tV@+`_etaLzmWLxJId^B*zS*&uU8o-D{;+?*rDF0+*4dC>6yi@V=@y0%hAJ#q&#o@v=?6cfQhD2;x+Zi4?OQ6cDPL}Jjnu`sw z*P1M8@whEUwiiO5OR)G4Cfr_Ga*uhhQ3Jc7lf`cw7VF33wd$5FlO-+z<7K`nvM1W` zIev=K^6qV@yQ0_@oCnI7u>XfAwNvEF{_Mk3Bs~c+qO%(Q+}a$SYP7xlm!2%>yfuG9 zc27odAAWGuBO!L1ScJzrSeizC{1-7m^U%$jA9PkrUwVTMr)1zn$a zeK_N~zL{eLj%1vQ(OlZ0k>;HG74XjH-)MM)#XI*lDyWx*9`HLBXsS0=oGD`}V0?RL z>T~Fb=JIguBuVqeUd*&v#^e>#vf{c8w@1VyUWSgnn4Dey-M!6F&&@V!bb6Qh)`rI$ zpyTVeW(e*2YF zT}5jdH{klBxm~(3;hgHXw4l?ikDlu#2^YEUrMcu)UFzl>*;NKn9W1YT*#AIS90R4n zB%7zdYhC37jW>C&gqB51cbO+&R)Bi?X{)OT799?e=tCT7*L~gV{^&-_R_R^>#P5OC zy{*2T+;G0p_0pD%E1i6%!5t{5ThK0WVNLrjo_%K22hGdtF1*Wj=m!@D+BxRh5KCm( zT0Kzg_lT31=9wCwnA9JGz@Hz@sMt0t00T9O7Atk^(BH#!k-Uo~quqvD9YvpI@!C_=_m^6uQ*@XI$(&UVqG(uZjCIJ94mPYN1;;$&?- zG>jT;@Cc=IPTLw&Rh|yqEL%PIZNGK!6l`q%y`yZAwCWgS7gp0;?ZL3*xcBWLR#@u&U47^ag;pwR{AHcfAg~NniwrJx4YrOf<0BESo`PVfGsqhqt`<}o+hkwtR6`;%;w0BI;!SGWZ z&*qiJ`9dLaMO<1mLudiM0j!KZ(Sss~qe?PvJn8e*@v{MNVX~3c5cpwxczI;}Pq$P5 zwqbYQbc!Axoy0nSwQbttPF*(^feVV&7wzERYXGw~G)8bm5nPGg1?%2_GKL$aj0o+- zJnnAr!os%;44Qx$5~CUhKs>xmiP4|Blz1Rj%W8Se+HeoSQVq}7RHo8;XS8UZ`-?HU zKMWseU;Pu~Q~LFH2%1&s$BDm|_|4+D8oI1E{b5dO*+oW|>$quijXcK^s=+7QESl!O z3)ftb|&#|R*uXy zE8sUADQ(Q12(x!gT%VY@eo6Ahhli7fJjtWG=&h;ghsL%<+K}JQc4{8YR{nRlwV`BA+#6!x>-=5oYVK%$38sGojeaEQ&n{SE4;n|%1PHkh9 z@!9Z&ciTEs>N&9(12Ld-&z(>2f`%+H7C+&Ox(+$gSrJ%p2%QidGt@g{9$?dTt zV?Gm{wnQHOZmnF_yv{icl@5>o$;pHDY)tkl-Kk5`d{tP;xqMMot8Ra`%t%FU8MCe0 z-8UmyFc<{jo`1RY{0&bT%Pcomo9(s^+xDb_SAA+QA9C@oHOO8Uu57v!ua8NMaNmTD zNj_UZe|KoJT%6$Fc+9!yo>h}Qzu~{Dzgazk-d>`SI+Zcv*Iyw$t75?uR~ZvT#}!7s27jG9W2e3KAw7F~jrUz4D=TZ2 zElIWI6ujj#i)_OR`J=Md(DL;PT~7DqRq&{?Y!%H(V@dx*Y~S{=F@BW%;L7!3J6$+i zaG{3s+553vsge~z9y0=Y_#Gae)~%|QQc3-H8lEkaQx%bw)+^d&MN zHFiLwDgsLXdS&A7r-yc{fG{rI{7gdqQ3)Sa(~4N0d?p*KX|>FNEp=DRteVK){~U~^ z#cJ7u;4BfVar8a5fQ*813(Eh6Q5y3XhppIf#FYc1pKnbLj3wa0_LAuPM?ln@jvKRF zjHCtFbM|}Z3z@;#R3=pH0hUc`Qk)QeEpc(q2Z24K6-Wt~(FM6A8nNkkwDeegPCI3B+- z`n$>@t+(qw^jm`w>tsGPcdwVjG{0Lfe+43h#v7!0bzFDaAY-ay)tS7(D4P+NOF12n z@)@q;48q%`8;!y4Y23nb#qxd1XQF61EYD{f<#=`Nq(%EmN(P}~H`l=JM3v3rH(M*_ z1&taBn~leOu0n|cybHt<0iE4Jc?p~W4K~dB#n!}lZ0Lxg#2hYjtO}WZ8jrl% z1Z$c+uZos*af=l2K~7+X`Dk92N?WD3kG43d-BzO%AC>-UR8-_j+(I^ngfnn=m>j-u zABzgR@OwH}h`5Kuzv}(AO3x6~a>7>G9Sn2iR_W`j^+ebUd5I{l`=YonZZT4gO49dh zJzUB;k1f;gcrizn2w57WMP&2k5v)zIciCnPo9kMSI1m{&i#zx#S~EH``q*vqNAN$Q z)pKU_k-270r9lXKrs1b@w#cY!pR(QPsO}RxzvpS|c~%u{O}tLCU9N|q1(=bk$R(nd zUYOZ4n$OIr?+)2m3x0<0kk_?P0Mm9D1#l_%%v+r%&s>er*(T72r|pn{+G=ghN-C+R z(w=i=sASl4t`swY)$3@DnfFn3jM`>Zlr>XWW0z5RB@gS;$1d4LQ~9u%a`Sq{ZW+`D z?(26;XsFgO-_G5}Q;~_^$f?$dVc|DMue3CHvEjsff6T~=Vfi<*CltMI3pBW0zJ5;w z-(asq-eIXUd=D3nRbwVU|8CV|kJoVFEjaRn*b3Ey-(R4?S#0S`1LPu-LF3X3?vYas(AO)6q3xLkv3`$4gdy0S2$qw`SLKVl%=j>hw>;dO!BFv*W~NV5u1qOb zAM1;pYbM`5=~Ex=(0{+|>V#C)*)JR0!)&@=Zq`R?-E{Ek|b0l;4VPkYT?(H7tc`%j>Pc^N~JS@8#BG`q8WmFSr9)c$K^O6pXyY=OU z<{Xt#6wLC^hh-x)G81klSXI9CX^I`k8I81YnHpx6RoIzwGgW@ajDf<(KC$Pb*?XEG zF4fS?!oj`!!N#b%_m7KTIJTeASOy+9`qB1HKOWeeyG*OB1e(!}J1%KWk(EWz0ldR?GyMb)m8rv>iUU5BhD*X5l3rZFP86*%j^Hk6eKeHs)@ zJUrgoVovRJ~9j8t+)A)1F*XyLc|7#FGQ@Q! z%0)Hmsb{2WE3IYFU(hIxlCzbO0?mIbSu?BTN`7yQL;(d}{oLO3JT`I6G7b4&*0)04 zwu1&AcFw)EBz@hYQjM}Sa&GNrOEPC9qcdgI^_lNw*8A`1*cjcv3`gfPXS()h7h>YF zn8BE1OvG6Ur~I9zW%^aKUgl_sKuU6UG)*&&|BS<)^Az~#oGeEG|M&J_)S{U#Gc#tb z{)e*QOeYW7W#9Jvzo#XK;Egn?k=!!#f;hE9);_&p43DLYYIAN(oG>=4C2hMP>0MBI z$1g}vXqs&=vvh+l%36ld@S-t0xxN@ztW;Pb_IEXZ4L>Z`sgd(=Z_eda+h?0s)Ld>c ztMlzC)y#sKu$=T%+mSvU5I8)gbwJxbj{Lf!ZP)I0?$x^8EsW7gzxCdJ$>>v4Z+m$( z?EVCM0_H4x=#rd-U;8C!aO9pk@vm>GX@4m}&uOsSAO@fvmFN+uE@1O^ljkQMj z$SIR-pM29lD;>|_!p%<4l*pNT4chmMQNyQT@~$H={Pw!e<*SCm zi&{|;!(T;ZCG)nLN&{I|Hp3a|Xj@UD0{_*Ey20X}9?o5_O(k%&tM$lAfaBXvXg_GyE74Njp(pawItz4vH zpyTwPS7f0W%j*$gdL^ZC_-R_HQ7b#l%hIR|7v?IDb7+-qj`NRaxwN@0J?Zxg zlqy^L;oNPc@$J}sx<;_Ce~807>q>V)U9E**`#bAO*ZBLUTxaM`$Q^r2!Qu_?EL}h3 zP*&*gtSjAbty|(73BN5>nOz>=x3T}iANFSXeP>KgB?E8XSlEqP7958;=kIz?OCzn62ZVOf6PSy#GW)K$ygNZC8< zO4q1+Thge}26e_35!CYd))jL%kH`vr@Sn1|x7IV??x*@kXlYHJi9HHFIy^IG7%5Y) z>JM>s-Sr-cNBVg1@J!lJV*)fddu@&Wyw&a=Gj{13c&icnG0$W|6h^D9&tx0SVwa%F zaiIO>5f!W0YxcD`9(g9Oqp-7e^O<<{LGGVGgX#Nf&L&I2^pg$h?kk^<{C*c<0mgApOk*JppEfDk95@8Ilg4nF+yr2vwP&f4_$yPW_N3b^d$G9{nZDhawL-C&qY-A*-UzJfVh4 z4nPxoXB_&|#T>%K#QgYg@r}!>BUw5CiOr#^vlXNDjXYqKCEggThlgp$+Egz8!o2GZ z%<)AQw0G5B?LqMCgi=4cdUD~5wkAKSQpa9G<2N#RAVTj34bEymjNj|#dh*fetcH+t zy_kh!w)(+uteCojxKqSgb@DgaSxw_06=KcE8iK|%}niW6c2A8{$>uq(DKRuU~yMk6n zxsPZ(OG1Uc!P;Tyw3`dCi{M+|FHy#i){kE=acw%@by$1EEUPWMzUS=Z9fDv)p zC?o*c`e-~%HCY6|bJV~`P`Gc&@2KCM+8d7@a{D5F_}*x)sAn`I`F$u_w%J6?$c!$a zmyB6b`U1infXv*PiOeQ{r@opb-YUGHGbjW;$(w$7O#TZeuD;S!h6jv+n52#Sta7cq zmw#LDN>Rgwk0OjDx|Bj&&v^(n21Yj(pFL+q^#nHPKRtmpK5O`EPwwkNel~X8%t;RE~2p9A4N_ zznAOO&C$N|YKHmoE8_hxO)xY5w?{P9L^IR-c7HM}%50_3e}4i#s{g^oQNOPW2d^Ai$7nTx??vurhl$CWR6-6Mi+InsM|(qFd5H7 zo!lKAN+8zg8AWT)J@!>^sAIXr_j{_i%iPI`$cWulu1wZ~>^a{ahPsRI6jY>fGw5dA z1GaRRQB!aa*cGu#QSzYx}GZ+0a>Ps#Q_1yc{i1Ly> z&sYj5kl zeTCB5Eds0|3HTZm1OW-Enq;-8`I`5 zO~)_#uJD*CEyc-k9YW)MOb&H{#-DS6zSV&wbRI^@@2VQDZ%)Sk{V}Eer||W7ysnKd ze6Om^nx|d2Xg)G{KH70FA6YveSx@khgY$9Vh~e%gdCudr_4Nz1{`sbCKtbYf^+xep zsD)YZ=4-@4txf9mXycyy(^VtVE*$**!JwF=mnR9rtHgkRK(OVJCOr#PL$u|4^HZ96HeZR_bD zV7|T*8tgjE;;-_DM*IP8=>N7qqgu!}M2jt4ZMt zS}E}zS;|&Y8lBVXN`tj{ zxfYlCW!*WgfUGZND=oWEX;mfndCgl^UDW2u3O5A0{*sng7G2U@WymG1$ig2qXW4p5 zD=c$<(29vy30rQNO^u98S|M9(e4ofc;(bfAOD7*23htU#IHO&D+miya{IXUkQ$3HY zaJN;F_-i=G$oDb=gJLNoScaGjKVu&qmn_aD5Py70n@ujd+kzz5K9nJYG{U*)fvk-5 zuoaeN9=1GmC~hRG$B#&l^Btu4^lhzNfj-g6(I~mziSa{|2&k3)sRDv&76K=w}s0r zPumZY+tW5kF5cAo&~H48%{O1LZ*sp8QHk+MNwWJ#%{xPW)cQM$>ur?8^4q9{CbzY& z(&jqqv)&JyEniAZTyo9G#Kg$qvMIOCF5`3Crm-;WR&32n{k723i{GL`?60U0{ z<=#E~=XXu3C-tu3lgMc|&|Jn3w3W`-c0=<*-AqHPu5(Mfl=14Gw#?PY%l7M9Gih`K zek?b%_oVGTl*s02G{xVoXmKm8Ha}@~R~sxZtu}Y(FtqUAgQ5q;^sa`^8;J()y%MQ^ zXtm{&8NX?hGD0+4nxov+Z29v?#j`)eCHo}DNW^wjCIejfo7PT_-qGrd{jTPmdvN@q z;r@YsQvW-IqDT1kU9F*n|AvYza!;#=MqE$Y{eUFBzKSk#{*qRiwg6eZ!7O*T>sl~- zsoltqJr7!9o$F|NdVOEYV=EYf{x@h)bTSeYhxUd3iH}lzB2$w4$0Wta#wQN$9~0%1 z64w_R{Rg36B=z@=Ns3NLK?iYuh;Tp8XM0b|=d+D7%DhQl+cnyX3j>#vd29paT1i`= zoOOd9x_2Q@zCFtd4@(|CI6A4?(4=e;4b^MZGrJ_InoqTVTuZ49S~>addo90XbxG)G ztC)W%b%rMS;6ouv=wriD;}9DB&Lmr5$C}c%C`N>DPH7b#LnQKZw2kF%wjxr=-PVl> z(+7q;677b{f4?ZaLRJ}?-W#+sGT6&jHaC_;Osx#?w%I+ylHvzN#zj^e8r?e(qccAU z@#zyC6`vTH9G@s3Vz}~+PVVoMI5^5DIx!I+ZVgC^k25ag`o+XWCnaP2i#0kizDVpl zTpv+O^E*#ufrbxeNq*vhC}Wa*A`|M zu6d(Skv>e4^q*^MQ`nDT^p8yPNg5v8JARPSSL#o*h05R=HkShZqLY2lkrHF#l49hM zANtYq8MX?tc&e?s{5r!HEVV;zzVc=&N-uOKTKvViHqU%W0b@Zkm&om?eW!T{?(sC6 zhfG?5!fQPbH9dDXv`;L=$eoZ3*`1ZP_dT*_MX%4yoJr3(TlsSTDiWw|8c^65bD1ibT&t_EG_9?a|XeEwQZEYmsEA;i+Rcr+uWsf(8 z?Wx^S=fz6fUh%)OayAx;z2ar7;>q@@7hYu2@VF?S*p%exVXReS(t5NW`F1OdjJ7Rf zJ1B+A*cwQSvbNHWK@wNiR@@lg=U2waP`I+q%_R^Yv*>}T=H4f|cS=9;t8D9Iw9>QX zZSfLP!&X27e2|JJRc+VG{j*siQ~wnB=&MPI{7oyJ`j6%aP1P`=5?m8Y8g?_)WQ}i% ze9*h&Q<5Y5p-~NtjvgF2C}v1>?-;aAOfC9HgMA~Dl6{Pk35G0{b zWO6k0QvMfN!EoA--gihC^gI5su#rvgGRNZxT+anGs=o8wvthX z9T6B7_)`_E>O_rwJ<;*l_o_$5$Hqp+^@&c5P73lFoERS)lY}zRyK-iU_#{R4jc(qq zb$C0yOY8l(de$`}%ep{q@S|c>2E!N}nV1wET9>zN!CrL&WksUR?rMBB2Vd&@IUn5O zV;SYVMk`ZH_rTPfo)Mn3WykbeqvbBGsz^_e(#onhT`D&!+sc*t#~=#De_Z=imrXWX znZo?M5N=o2WL zqHSd=_vTl+l+r}e@hBZsIEId7pXj)#cz!tOqc?E9!Ylq;4$tq>9=pO;&ifxdl(A&* zv1ktdGH18VyCTO4exRsp7*;s4ijb7PF@vIgQW9f)lH=upqs_Zy*0s;z$Ydryn~IM( zeyLRo{O3Rcm46CQmn!uBDd;jWBcgp`^@+wk>zb*_b|wBkm>Rlm#p=sxz0Iq_JI5zx zKp&0Nc%=$od@pGEB=v&UK!%>zn$nhE9$mm)1}>d6%)`-A;i6W7dTr&K^IDj6y?{9b zwp`Nmdn~0}oyQ^xvJx_{xh+D%YuG-KA5Lo(aL^)(NSv$Lib=>Btf7jW(Te6ziA2eu zw
diff --git a/src/components/CategoryBadge.tsx b/src/components/CategoryBadge.tsx index ef6a6474..e94c1f27 100644 --- a/src/components/CategoryBadge.tsx +++ b/src/components/CategoryBadge.tsx @@ -44,7 +44,7 @@ import AgricultureRoundedIcon from '@mui/icons-material/AgricultureRounded' import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded' import PhotoCameraRoundedIcon from '@mui/icons-material/PhotoCameraRounded' -import { cmn, cls } from '@skalenetwork/metaport' +import Ship from './Ship' export const CATEGORY_ICON: any = { hubs: , @@ -90,15 +90,8 @@ export default function CategoryBadge(props: { } return ( -
- {getCategoryIcon(props.category)} -

{props.category}

+
+
) } diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 09c1f80a..6fee1ad6 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -30,14 +30,11 @@ import { styles, PROXY_ENDPOINTS, type MetaportCore, - SkPaper, - getChainAlias, - chainBg + SkPaper } from '@skalenetwork/metaport' import { type types } from '@/core' import Button from '@mui/material/Button' -import { Container } from '@mui/material' import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' @@ -59,11 +56,14 @@ import Breadcrumbs from './Breadcrumbs' import CollapsibleDescription from './CollapsibleDescription' import SkBtn from './SkBtn' +import { chainBg, getChainAlias } from '../core/metadata' + import { MAINNET_CHAIN_LOGOS, MAINNET_CHAIN_NAME } from '../core/constants' import { getRpcUrl, getChainId, HTTPS_PREFIX, getChainDescription } from '../core/chain' import { getExplorerUrl } from '../core/explorer' import { formatNumber } from '../core/timeHelper' import ChainTabsSection from './chains/tabs/ChainTabsSection' +import Ship from './Ship' export default function SchainDetails(props: { schainName: string @@ -89,7 +89,7 @@ export default function SchainDetails(props: { chainName: 'SKALE' + (network === 'testnet' ? ' Testnet ' : ' ') + - getChainAlias(props.mpc.config.skaleNetwork, props.schainName), + getChainAlias(props.chainsMeta, props.schainName), rpcUrls: [rpcUrl], nativeCurrency: { name: 'sFUEL', @@ -120,7 +120,7 @@ export default function SchainDetails(props: { const chainMeta = props.chainsMeta[props.schainName] - const chainAlias = getChainAlias(props.mpc.config.skaleNetwork, props.schainName, undefined, true) + const chainAlias = getChainAlias(props.chainsMeta, props.schainName) const chainDescription = getChainDescription(chainMeta) const isMainnet = props.mpc.config.skaleNetwork === MAINNET_CHAIN_NAME @@ -145,56 +145,77 @@ export default function SchainDetails(props: { return (
+
+ , + url: '/chains' + }, + { + text: chainAlias, + icon: + } + ]} + /> +
+
SKALE Portal - {chainAlias} - - -
- , - url: '/chains' - }, - { - text: chainAlias, - icon: - } - ]} - /> -
-
-
- -
- - - - - -

{chainAlias}

- + +
+
+
+
+
- } - /> - - +
+
+
+
+ +
+ + } + /> +
+ +

{chainAlias}

+ +
+
+
+
+ + - + = ({ label, icon }) => { return (
- {icon &&
{icon}
} + {icon &&
{icon}
}

{label}

) diff --git a/src/components/chains/ChainCard.tsx b/src/components/chains/ChainCard.tsx index d6be8baa..4d23814f 100644 --- a/src/components/chains/ChainCard.tsx +++ b/src/components/chains/ChainCard.tsx @@ -75,7 +75,7 @@ const ChainCard: React.FC<{
} />
diff --git a/src/components/chains/tabs/ChainTabsSection.tsx b/src/components/chains/tabs/ChainTabsSection.tsx index effd9ca2..91ffcc1f 100644 --- a/src/components/chains/tabs/ChainTabsSection.tsx +++ b/src/components/chains/tabs/ChainTabsSection.tsx @@ -37,7 +37,6 @@ import DeveloperInfo from './DeveloperInfo' import Tokens from './Tokens' import VerifiedContracts from './VerifiedContracts' import { getExplorerUrl } from '../../../core/explorer' -import Headline from '../../Headline' const BASE_TABS = [ { @@ -107,11 +106,6 @@ export default function ChainTabsSection(props: { currentTabs.unshift({ label: 'Apps', icon: }) currentTabsContent.unshift( - } - className={cls(cmn.mbott20)} - />
- } className={cls(cmn.mbott20)} - /> + /> */} diff --git a/src/components/chains/tabs/Tabs.tsx b/src/components/chains/tabs/Tabs.tsx index edd6eb86..be952c6d 100644 --- a/src/components/chains/tabs/Tabs.tsx +++ b/src/components/chains/tabs/Tabs.tsx @@ -31,6 +31,7 @@ import { cls, cmn } from '@skalenetwork/metaport' import { type types } from '@/core' import AdminPanelSettingsRoundedIcon from '@mui/icons-material/AdminPanelSettingsRounded' +import { Button } from '@mui/material' export default function ChainTabs(props: { chainMeta: types.ChainMetadata @@ -61,13 +62,13 @@ export default function ChainTabs(props: { /> ) : null )} - - } - iconPosition="start" - className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab')} - /> + +
diff --git a/src/components/chains/tabs/Tokens.tsx b/src/components/chains/tabs/Tokens.tsx index 546ed6fb..d98b0398 100644 --- a/src/components/chains/tabs/Tokens.tsx +++ b/src/components/chains/tabs/Tokens.tsx @@ -24,10 +24,8 @@ import { cmn, cls, styles, type MetaportCore, interfaces, SkPaper } from '@skalenetwork/metaport' import Grid from '@mui/material/Grid' -import AccountBalanceWalletRoundedIcon from '@mui/icons-material/AccountBalanceWalletRounded' import CopySurface from '../../CopySurface' -import Headline from '../../Headline' export default function Tokens(props: { schainName: string @@ -44,11 +42,6 @@ export default function Tokens(props: { return ( - } - className={cls(cmn.mbott20)} - /> {Object.keys(chainTokens).flatMap((tokenSymbol: string) => { const wrapperAddress = findWrapperAddress(chainTokens[tokenSymbol]) diff --git a/src/components/chains/tabs/VerifiedContracts.tsx b/src/components/chains/tabs/VerifiedContracts.tsx index ba1e9a99..173ec48c 100644 --- a/src/components/chains/tabs/VerifiedContracts.tsx +++ b/src/components/chains/tabs/VerifiedContracts.tsx @@ -26,12 +26,10 @@ import Button from '@mui/material/Button' import Grid from '@mui/material/Grid' import ExpandCircleDownRoundedIcon from '@mui/icons-material/ExpandCircleDownRounded' import HourglassBottomRoundedIcon from '@mui/icons-material/HourglassBottomRounded' -import PlaylistAddCheckCircleRoundedIcon from '@mui/icons-material/PlaylistAddCheckCircleRounded' import { cmn, cls, styles, type MetaportCore, SkPaper } from '@skalenetwork/metaport' import LinkSurface from '../../LinkSurface' import { addressUrl } from '../../../core/explorer' -import Headline from '../../Headline' const BLOCKSCOUT_OFFSET = 20 @@ -73,11 +71,6 @@ export default function VerifiedContracts(props: { return ( - } - className={cls(cmn.mbott20)} - /> {contracts.map((contract: any, index: number) => ( diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 48c0c69b..3ef6f7c6 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -46,10 +46,12 @@ export default function AppCard(props: { }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) const url = `/ecosystem/${shortAlias}/${props.appName}` - const appMeta = props.chainsMeta[props.schainName]?.apps?.[props.appName]! + const appMeta = props.chainsMeta[props.schainName]?.apps?.[props.appName] const isNew = props.newApps && isNewApp({ chain: props.schainName, app: props.appName }, props.newApps) + if (!appMeta) return + const appDescription = appMeta.description ?? 'No description' return ( diff --git a/src/components/ecosystem/CategoriesShips.tsx b/src/components/ecosystem/CategoriesShips.tsx index 9a56c55e..35cc109b 100644 --- a/src/components/ecosystem/CategoriesShips.tsx +++ b/src/components/ecosystem/CategoriesShips.tsx @@ -24,9 +24,9 @@ import React, { useMemo } from 'react' import { type types } from '@/core' import { Box } from '@mui/material' - import { categories } from '../../core/ecosystem/categories' import Ship from '../Ship' +import { CategoryIcons } from './CategoryIcons' interface AppCategoriesChipsProps { app: types.AppMetadata @@ -47,44 +47,27 @@ const AppCategoriesChips: React.FC = ({ app, className return category.subcategories[subcategoryTag]?.name ?? subcategoryTag } - return Object.entries(app.categories) - .flatMap(([categoryTag, subcategories]) => [ - , - ...(Array.isArray(subcategories) - ? subcategories.map((subTag) => ( - - )) - : []) - ]) - .slice(0, 3) // Limit to 3 chips // todo: remove restriction! + return Object.entries(app.categories).flatMap(([categoryTag, subcategories]) => [ + } + />, + ...(Array.isArray(subcategories) + ? subcategories.map((subTag) => ( + } + /> + )) + : []) + ]) }, [app.categories]) if (chips.length === 0) return null - return ( - *': { - flex: '0 0 auto' - }, - gap: 1 - }} - className={className} - > - {chips} - - ) + return {chips} } export default AppCategoriesChips diff --git a/src/components/ecosystem/CategoryIcons.tsx b/src/components/ecosystem/CategoryIcons.tsx new file mode 100644 index 00000000..9b39f254 --- /dev/null +++ b/src/components/ecosystem/CategoryIcons.tsx @@ -0,0 +1,153 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file CategoryIcons.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { + AccountBalanceOutlined, + StorageOutlined, + ShowChartOutlined, + CollectionsOutlined, + TheaterComedyOutlined, + ExploreOutlined, + SportsEsportsOutlined, + HubOutlined, + MemoryOutlined, + FilterFramesOutlined, + VisibilityOutlined, + MoreHorizOutlined, + HandshakeOutlined, + SecurityOutlined, + PeopleOutlined, + BuildOutlined, + AccountBalanceWalletOutlined, + SportsKabaddiOutlined, + CasinoOutlined, + StyleOutlined, + BeachAccessOutlined, + TvOutlined, + SportsHandballOutlined, + VrpanoOutlined, + PhoneAndroidOutlined, + ComputerOutlined, + GamesOutlined, + ExtensionOutlined, + DirectionsCarOutlined, + AutoStoriesOutlined, + BedroomChildOutlined, + SportsSoccerOutlined, + PsychologyOutlined, + SportsBaseballOutlined, + PrecisionManufacturingOutlined, + AutoAwesomeRounded, + HikingRounded, + PaymentsRounded +} from '@mui/icons-material' + +export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { + switch (category) { + case 'ai': + return + case 'dao': + return + case 'data-information': + return + case 'defi': + return + case 'digital-collectibles': + return + case 'entertainment': + return + case 'explorer': + return + case 'gaming': + return + case 'hub': + return + case 'infrastructure': + return + case 'nfts': + return + case 'oracle': + return + case 'partner': + return + case 'security': + return + case 'social-network': + return + case 'tools': + return + case 'wallet': + return + + // Gaming subcategories + case 'action-adventure': + return + case 'battle-royale': + return + case 'cards_deck-building': + return + case 'casual': + return + case 'console': + return + case 'fighting': + return + case 'metaverse': + return + case 'mobile': + return + case 'mmorpg': + return + case 'pc': + return + case 'platformer': + return + case 'puzzle': + return + case 'racing': + return + case 'rpg': + return + case 'sandbox': + return + case 'shooter': + return + case 'simulation': + return + case 'sports': + return + + // DeFi subcategories + case 'custody': + return + case 'dex': + return + case 'yield': + return + + // Default case + default: + return + } +} diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts index ede695b7..f42d3b09 100644 --- a/src/core/ecosystem/apps.ts +++ b/src/core/ecosystem/apps.ts @@ -21,6 +21,7 @@ */ import { type types } from '@/core' +import { getChainAlias } from '../metadata' export interface AppWithChainAndName extends types.AppMetadata { chain: string @@ -73,12 +74,15 @@ export function filterAppsByCategory( export function filterAppsBySearchTerm( apps: AppWithChainAndName[], - searchTerm: string + searchTerm: string, + chainsMeta: types.ChainsMetadataMap ): AppWithChainAndName[] { if (!searchTerm || searchTerm === '') return apps + const st = searchTerm.toLowerCase() return apps.filter( (app) => - app.alias.toLowerCase().includes(searchTerm.toLowerCase()) || - app.chain.toLowerCase().includes(searchTerm.toLowerCase()) + app.alias.toLowerCase().includes(st) || + app.chain.toLowerCase().includes(st) || + getChainAlias(chainsMeta, app.chain).toLowerCase().includes(st) ) } diff --git a/src/core/ecosystem/categories.ts b/src/core/ecosystem/categories.ts index 1079a1c7..190aee9f 100644 --- a/src/core/ecosystem/categories.ts +++ b/src/core/ecosystem/categories.ts @@ -83,5 +83,6 @@ export const categories: Categories = { security: { name: 'Security', subcategories: {} }, 'social-network': { name: 'Social Network', subcategories: {} }, tools: { name: 'Tools', subcategories: {} }, - wallet: { name: 'Wallet', subcategories: {} } + wallet: { name: 'Wallet', subcategories: {} }, + metaverse: { name: 'Metaverse', subcategories: {} } } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 7f5dac00..61896c50 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -179,7 +179,7 @@ export default function App(props: {
- + {appMeta.contracts ? ( { - const filtered = filterAppsBySearchTerm(filterAppsByCategory(allApps, checkedItems), searchTerm) + const filtered = filterAppsBySearchTerm( + filterAppsByCategory(allApps, checkedItems), + searchTerm, + props.chainsMeta + ) setFilteredApps(filtered) }, [allApps, checkedItems, searchTerm]) @@ -123,7 +127,7 @@ export default function Ecosystem(props: {

Explore dApps across the SKALE ecosystem

- + * { + flex: 0 0 auto; +} \ No newline at end of file From aad359872e0972a3ec7f5f90aad6b24c85aed937 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 13 Aug 2024 19:56:06 +0100 Subject: [PATCH 21/39] Improve ships component, fix trending apps --- src/App.scss | 7 ++++++- src/_variables.scss | 2 ++ src/components/SchainDetails.tsx | 2 +- src/components/Ship.tsx | 11 +++++++++-- src/components/chains/ChainCard.tsx | 2 +- src/components/chains/HubTile.tsx | 2 +- src/components/ecosystem/AppCardV2.tsx | 2 +- src/components/ecosystem/CategoriesShips.tsx | 19 +++++++++++++++++-- src/components/ecosystem/CategoryIcons.tsx | 9 ++++++--- src/pages/App.tsx | 11 +++++------ src/pages/Start.tsx | 1 + 11 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/App.scss b/src/App.scss index d2bb127d..12d5e531 100644 --- a/src/App.scss +++ b/src/App.scss @@ -259,7 +259,12 @@ body::-webkit-scrollbar { } .border { - border: 1px solid #171616 !important; + border: 1px solid $border-color !important; +} + + +.borderLight { + border: 1px solid $border-color-light !important; } .radius { diff --git a/src/_variables.scss b/src/_variables.scss index 346e8028..3a275190 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -2,7 +2,9 @@ $sk-border-radius: 25px; $sk-bg: #191919; $sk-bg-prim: #000000; $sk-btn-height: 47px; + $border-color: #353535; +$border-color-light: #171616; // legacy $sk-paper-color: rgb(136 135 135 / 15%); diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 6fee1ad6..61dc8ea2 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -173,7 +173,7 @@ export default function SchainDetails(props: {
= ({ label, icon }) => { + onClick?: () => void +}> = ({ label, icon, onClick }) => { return ( -
+
{icon &&
{icon}
}

{label}

diff --git a/src/components/chains/ChainCard.tsx b/src/components/chains/ChainCard.tsx index 4d23814f..35f84bcd 100644 --- a/src/components/chains/ChainCard.tsx +++ b/src/components/chains/ChainCard.tsx @@ -60,7 +60,7 @@ const ChainCard: React.FC<{
diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 3ef6f7c6..824a8ed4 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -60,7 +60,7 @@ export default function AppCard(props: {
= ({ app, className }) => { + const [expanded, setExpanded] = useState(false) + const chips = useMemo(() => { if (!app.categories) return [] @@ -67,7 +69,20 @@ const AppCategoriesChips: React.FC = ({ app, className if (chips.length === 0) return null - return {chips} + const visibleChips = expanded ? chips : chips.slice(0, 2) + const remainingChips = chips.length - 2 + + return ( + + {visibleChips} + {remainingChips > 0 && ( + setExpanded(!expanded)} + /> + )} + + ) } export default AppCategoriesChips diff --git a/src/components/ecosystem/CategoryIcons.tsx b/src/components/ecosystem/CategoryIcons.tsx index 9b39f254..bfe330b0 100644 --- a/src/components/ecosystem/CategoryIcons.tsx +++ b/src/components/ecosystem/CategoryIcons.tsx @@ -54,13 +54,14 @@ import { DirectionsCarOutlined, AutoStoriesOutlined, BedroomChildOutlined, - SportsSoccerOutlined, PsychologyOutlined, SportsBaseballOutlined, PrecisionManufacturingOutlined, AutoAwesomeRounded, HikingRounded, - PaymentsRounded + PaymentsRounded, + FlagRounded, + FlareRounded } from '@mui/icons-material' export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { @@ -132,11 +133,13 @@ export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { case 'sandbox': return case 'shooter': - return + return case 'simulation': return case 'sports': return + case 'strategy': + return // DeFi subcategories case 'custody': diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 61896c50..09b7630a 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -142,7 +142,7 @@ export default function App(props: {
-
-
+ +
-
- +

{appAlias}

diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index f26eaa67..2ab924fa 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -77,6 +77,7 @@ export default function Start(props: { if (props.topApps) { appCards = props.topApps .slice(0, 11) + .filter((app) => props.chainsMeta[app.chain]?.apps?.[app.app]) .map((topApp: types.IAppId) => ( Date: Tue, 13 Aug 2024 20:27:31 +0100 Subject: [PATCH 22/39] Update appcard component --- src/components/chains/HubApps.tsx | 2 +- src/components/{ => ecosystem}/AppCard.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename src/components/{ => ecosystem}/AppCard.tsx (90%) diff --git a/src/components/chains/HubApps.tsx b/src/components/chains/HubApps.tsx index 6406f4da..f85e67be 100644 --- a/src/components/chains/HubApps.tsx +++ b/src/components/chains/HubApps.tsx @@ -30,7 +30,7 @@ import { Grid, Tooltip } from '@mui/material' import { chainBg } from '../../core/metadata' import { sortObjectByKeys } from '../../core/helper' -import AppCard from '../AppCard' +import AppCard from '../ecosystem/AppCard' export default function HubApps(props: { skaleNetwork: types.SkaleNetwork diff --git a/src/components/AppCard.tsx b/src/components/ecosystem/AppCard.tsx similarity index 90% rename from src/components/AppCard.tsx rename to src/components/ecosystem/AppCard.tsx index e481dddb..72bb4894 100644 --- a/src/components/AppCard.tsx +++ b/src/components/ecosystem/AppCard.tsx @@ -26,11 +26,11 @@ import { cmn, cls } from '@skalenetwork/metaport' import { type types } from '@/core' import Button from '@mui/material/Button' -import ChainLogo from './ChainLogo' -import { MAINNET_CHAIN_LOGOS } from '../core/constants' -import { getChainShortAlias } from '../core/chain' -import { formatNumber } from '../core/timeHelper' -import { chainBg, getChainAlias } from '../core/metadata' +import ChainLogo from '../ChainLogo' +import { MAINNET_CHAIN_LOGOS } from '../../core/constants' +import { getChainShortAlias } from '../../core/chain' +import { formatNumber } from '../../core/timeHelper' +import { chainBg, getChainAlias } from '../../core/metadata' export default function AppCard(props: { skaleNetwork: types.SkaleNetwork @@ -46,7 +46,7 @@ export default function AppCard(props: {
From abea4b743115602b59d3743f0705ec570921a6c9 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Wed, 14 Aug 2024 20:27:35 +0100 Subject: [PATCH 23/39] Implement Favorites logic, add sign in with ethereum, update home page, add API connectors, add auth and like providers --- bun.lockb | Bin 585309 -> 586922 bytes package.json | 5 +- src/App.tsx | 8 +- src/AuthContext.tsx | 143 ++++++++++++++++++ src/LikedAppsContext.tsx | 121 +++++++++++++++ src/components/ConnectWallet.tsx | 32 +++- src/components/ecosystem/AllApps.tsx | 31 +++- src/components/ecosystem/AppCardV2.tsx | 7 +- src/components/ecosystem/FavoriteApps.tsx | 94 +++++++++++- .../ecosystem/FavoriteIconButton.tsx | 89 +++++++++++ src/components/ecosystem/Socials.tsx | 32 ++-- src/pages/App.tsx | 26 +++- src/pages/Ecosystem.tsx | 31 ++-- src/pages/Start.tsx | 14 +- 14 files changed, 563 insertions(+), 70 deletions(-) create mode 100644 src/AuthContext.tsx create mode 100644 src/LikedAppsContext.tsx create mode 100644 src/components/ecosystem/FavoriteIconButton.tsx diff --git a/bun.lockb b/bun.lockb index b47a1951f749010187d781d3b1c868449b8e560d..6c37e5664cf56facbfc3a3c10c268b06a9598f2c 100755 GIT binary patch delta 127701 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�)npIiLG;+GQn2-U$ci{D!P&rm^l zf_%06GbkPuvFPvr)vX{@!!<#9pcbgC1dDY+<<|$*=PAmxD7~T88(Ceba*aW?*WBtt zMSq77^RJ992x=h777!}g!*Zb->S?i;#oi$Q0{UM}3j68TA5cL(7+viTv*kvB($hlt zUm25Z#$-?(O|kq8P;)=u>Y1QELY2$1{J(=Ly1%d_V4mewu@3qwbY-ov z<<0~(mt|HzSDAc-O0KtDsCGAiDt{rUj<OEq4 zRg?jbTD>Y_NNM0nTk#oN;IB|Y^CIQ6lf7!o|4&ec=>M*&k3miYC8~L6JZul;w`{&p zG4he+RZ;DHY;~ddCzk(rP>%Y{mKWB8|D?sB4~hTJP}kNWS>q%9Yb4dIAQZ1|xlp#% z03|v6QhvOMMI{p~7b;oL;we@aDh3-_UWxu@^oI;}(AX9fCc-=029iJ-)B{v-_XSnH zAIQHze~SY_m5WOJ14$N)+MD@mxpDvPUafpaWh3+nS{sH}Ci+_|=#Q0<*(alOS2payyYD1$HbIrG1n zgg!!9cB$o6QM!k&E2(R3epQro1HXhf+x)61X$QY#*ljl7$JSPmMR$P1;1^Jy{LS*;E&6Un z7a|3!fhrdU)o~3C(xMz%%a*HSb)o7df+}C%>h-JJBddWGs-g-uv=tiJ{Hmz@#^`cr zORzTB#g?mz>Np8q<-6PbN-RatzR?fVh>}57NU;TlUEvFDey-L33KJ+_LOB_B7O0JW zgRLi2`x`AUjV1mJW$`8o3d=zmc#&;ji{)D_zYJ8l%RzmrqB`7Wb)k~i7+&fp)bO>o zKvh)58>}wW?a6M-{|Z%aFXiNc2S6G2pv8wk#mHkmXZ{~2p^s3>r}?D@4p{wvf*ME# z^%V6l+xkKo@Cv9^{wAn$Z;R7^;Ged@N1#4`g=*+y+rUv!RlczNUm|?|4AtQ=TkacB z1NG6{WYe`R&4tU&3j&U{%yeI$6CcY7TqY z27223{|Ra;dfR%vYm<;GlEG@;+fVb9^NXi>%31kK8$@6*xvDnQRujtXVU`P(Kf-dM z@<&=;6;*Du)rBfI#^PA33)OF$CP)P)*bJezqcxTbm47CvhR?RTQ2IHbwzmx+-@ycq zSp5x99lQf76yFE+`M;wdp#Pbnjy|M<{PwAB;0xP8Rg?i=T3x8*H^9Rg_-G>O%3lpgO8=^{Obnw1F+q7*vN%EjG8< z(qd~+71~+e9#p7yvw9D!_X0JbzMu@~XZZk|KhRTx|e@2IM~1Me99Jk z))qTx`SYOW;blJ_j|fW1!;T8;jr7V2!H4k5)Kg z@n?&_TKwH&5Nk9)AyDLb)h4_f|bsEetW$d|z{gKFnBP=>!H-D`g+Sl3^K??U+(_*gRd ziOBx~)yx;R{Fk=;pP_D+e1?@B<+^}!b2qF12{!dB_Mm|5>}xRv40`8apCdPqQhjfi z%vCq3P<1~+vPE?>NwvUqt5-$oldb+|sQf9m{#0A;Pq4RH>J*S6Gi-&K7H8RtLgmku z?&ZH=-JAv0Oy=2QRZ+#4qH7!lHoq#$zGYUgiki)0bmf=We4*~HO08ZN$Nz9n1=d;N zTu@ie7lU%-rJxMB91M9$FJiBXUj@~yTn~!h0O})@oi|x7RC9M(F4Q>h1J(S4Ru`(9 z{h;iA!s1!e+O0m70PQ}d<3fMm;6$E{QxTeM~Ob# z>pKa`I1b9VTR7aOk zPP4ep*1yu$tBTc#zrZy%qbkaPYppJnVK;y>=the-S^ZW}Q?LhA$M@O%`$2vF4Ass) zl~?>dY%A=y6@)7A2q@3I0IK1at^SI|S3w#2n#Ciao<4sH>LZllUx2EA%;I+zkAwa` z`!fl3_zS28e*@*Z8bl7sKwVJ05vcqopvpJ3*vw*ckbi*={8IVuA{NE_!4*3xHh*}$ z{s)mqV6J5a0fIH-?M{1?lG;tB{&bu|TsMSs7lfuM$Kg5ANspepnS73EQzKiJ|Bi$g&f zJlygTpguwcVXDPRpxQYDlmQtQX9*Smb4jSfESr%7s^LXeUjphQRJmoK3|?+=h0PaA zF9cP;*zyuk9j^ve|7^=kLH~NC%nIjOJm2Cbix+`fE|-C7_zJ6EY4IA1*MaKbCd+TN z{C10Xf->+fP#(Is7UzY&!F>`zE;<0}OnL#-+`b8_!FNG*{6479pP?H5fPCSH7XJyV z-A_Ta^QFbFK^gF4E&Q(v#}U+Fb$*G5E!L38M<~6f#W<@A)o^W42G+6pbwPat+{Wg& z2lWxEoeq|FC?%n}>1;E)SnO)`?w~B~1L`9b9{_4(gF!Vo1e5{8EFTW)GGh{`smlQM zsfwzfG{;t$Yf*(d+8-6@#*e1tbbe00w}}(7_|Zow2QA`9Gmy)VK0?jt5`LsF(Zg)<;w>d=jAbd`~w|VltO+rDBo&V&0O+Q4?PrkQl_KlP8ZB~6> zQ_&%=t>ol;n^oWA)S-Lwz0H&FZJI}XC*RvV`QGNq_cqNraq_)QzMj$D$jSFMPrkQ# z^1V%SK%RVWlMR6n?{S`dZ_~WTse6i(?``@~q4zgWzPD*yQTZY#4=i+(b@IK759NiihNN{O$7(-nhSO zenE6m?|+@&XjN{npMOZ-_{%$umoE&5-yGZf)9+sT_N>d#9}(dHZFkMMOTSH;6+V)( zV`u)dS8l!H)R!;wcGUH5QCzLRLXyA+KPwp7|+ti|SLY=$S_+iHTJ%4=gti&(V3J+(Nc0WC| zKGJ>POP5DqZyf9~?C{d$(lzU6u7B~;FL&&&)uLxi!1S@{V++*-r8F zGpJi=Tqq&neRg$dn)kYt{cBO`2E6)ZC>v{`l$3?WrRgs`VxQ`(e)@6TM;^U==ePHL z(D=hIF1zB1@ELtSdH$=;4d*qlzvHxl563;3{L;BkHy!!%*Hwmeiq~cwLpoX;bMtH9 zx(40@H4wHXgz9)LYlc$!F9zh*L>N`3L1^Kv zaS$@mXy5Jr2SN;n`Ptuew_@5;sqt6L%bDq);Awh2O~)(E?rAf$OGB)l$R zPE&-5-p-~78`~fxG($-DW;8=cZi}#A!W7SGj&M{$eshE~yayy~YlqOH1;TVMuLZ)G z(-00xnBg^PiEv!PIV}-pc?Tuj)*d0L6~Y{EO)G?q4hU~cnCEqFjS$xnVRLJQOz((< zeG&$>LCE$twL!@3gz%+=9It;{gvOl_wzoxC?GRRXLHJd|5^wBj z2%Wkj>^co0-#a1UbqRCYBP{cFwny054I!Ze!U}Ij2ZZD#g#8kVJf|bVQ3?4S5lXxV zBy8)B(4rHPoOgbm)O5*qhGNb8Ppfp=whgaZBUxZyf5H9piNa)lLVNOqk&EC$Q2(L>>=!LMwo6!qlV+3Kpgsq;_8zDIv zA-^|*>pdXhsDu`$BV6v~osO`rKf)mip4X%g!k7UF=k!6i(mN>OxP+v>2v>V+`Xbzx zg7CJ4?Ox}82pIzrHupog&O0I@ZVQR$c<8Bdom?% z@jjK%crZd*e}o<0mHiP8NcdI4PH*f0gw;b3b`3!In|DG&r=bXQQV@1|J5vx|myj?J z;Vy5+K!lCM5cW&B+j9mXBo9Z(AB3>idqBcb2`!=s_j-9zgl!`b4oSG*Ycd#N%t(ZD z1|vM+9h7ifLeda~hrBgI5N;cV@V12gUgx0*8KV(44@G#?J0c-&48ovc2#pWUACB;}_o;-&sR(I0R-g5*9D#5EVSMXT4zzgrvM1*}T{G*#hD|OS zxoO|-VZGZXP1`em*6UxlzxclH-`$`5eaCq(R@~RW)n2FVTW2JdJl~*4=Fq>jzH{c) zr`~J+d!puyJm@)NXm<5D`pF+dKhJp&Na!>kp~YB)Ltfrkgx4h;lJJ7pBo$#}8p1iL z2#39c5|Sq%Y#4{|vUk`&r6lwnkMOFuZagKnO+@%e!fRfyG=wpe5H3$cIO4r8;kbm6 z6A<3?woX8}Egj)|32%EtCn97_M!01n!n@wr65^&HoG}UEeeZ@z2>T?2(-A)O($f)g zry|@d;Uh0N8KLnR2sx7xKJo6Ba6m$XDF~l>nNtu}PeXWG!slM%RD@2`5lW^aeCa(d z;dKe^&p`OfTX6=$#tej4C4B9*nTC)&17X87gm1mW5{^pfJ00PBZ{2i+Z8H%*lJKL~ zD+6K7EQHH55KegSOE@lJUt~YBW%n@cvV88*CrDoc>%(POoUUs z!xD~4=$nPmz+0Dvuq_ASBMFVXUfBp^79w1pjnKq!s;f#d{ZM+*6BJ7h8UWCxjOJ9VLyBOhK3GKaLE<)oa2syb3 z9lg6H9FWi;523S{nTN1?DZA?$y$^g(<^TWxni=!o(wGp7F_pBm zOp=zC`4^)xNm^PaNy8Z8WickiXs!vdSSE?lG8I}HL;fs{A%9j@>yK(_HK}~>kGW2t zeSCUo<(#pxe`SZ0b>vyP23nn^7Dv7i6G{lctj5qF&+^ckI0q?GaeHVev=R} z6A(R3mPEdU??gl|6Fm`;Fd302(Z~42BLWi;@$ra$CRd_JB484tzlocKNS=Zyl!!3? zlMx|P5lNE~gG_-$sYGxBBGM!#AkwBGN+gDupecy(7ZIsb5JOF|M1@4yRKzfoG8K_I z9Z@bZ+=Na;M9n~COhb$?WfIf z8HiD)mK%{TG1iS3ZFWc`yo5M36EVg#O+*CFLQGFYj5YfuiX=L`gcxV0yo5-ejW{C_ zXWGv~gd`yr&O%HuCnZWHy3Iz!n|ZSlX>$-4B_^9LNr>>dh*e35Ddt;=3W8LOorj2>kJu@ZWNN*P z@LPZw`!Zs#*&&fH(PBPgo*6wKk+2YPNMgQex&RUQ3S#;K#6q)QqDZ2{Lc}67Wg#MY z5#o$QifR7}B4jaQ;VX!x=A=ZaM7KqVR5Nc8A}s}RQNoxmixJ^V5UUm=mYZ)SDkS=) zAXb>=DTvIah`%IOnO;k(4e@y0q${j8mlf8Sh^2&dvq9kv5-ZB1Zgw19O!6d&%ch02Ioh;LS zIex22RM=)t5=`j|7I%A%#o1=wYlyU!h>H^2O_vpj@KuOaD-b(O=t@L|M8-k(mZB2Ji;Hxc2Ph;oUOCUiZbLLy^5;6wVAw-8<%5a&$9288!UM7Bh+@puc7BN6i!;wzIS5&Jg6cO#<2L~lg+ zZ9?Qpd}DmxM&wJxzm53Tf)%;<5?dil~st*owGf z$|N%1MMQ2x{AJR&A)>MoUhg8VnuvE1-tQr@5!F1ZnQGa3l_3+8jq$8zw#vkA$N0WS zB`*{G9+muZ5P1?cjn8&OzC`?XL@kpmk+1_1kb|gW;&Kpy?;{E&>KXqXh$4xk9f$^| zKqC1AMDY6vACve#BIH9vi9{n4^Z}w&BJ~4=uPK&D+ldJK5Yfb>e256&g(#P3YC?A+ zDkL&?BAS^piOk)I$X$pQCVdwo>LY~LZbVBHu^Zu?i^!JnHy$4$awK9tLbNtn60sj6 zd~*>2COQ}4w+E3Y(a!jMjL4UW{}|EUgwhz(Sr0he4??;qNbTOg% zhzf~}d_-4MCXsmn5xF1H&7|)~L>)wU9YAz95eE?7pCYm)LXF2kM2piljv!D4k7X-;twHunOup4BZz>*h(0FnFe30MqEMor@jrqnl1Ms& z=x+)nk_!;QM-dSw@hBqXGen8RAQM!8D3wSpKt!5iiL_&gu+I=fOv-17@XrzD5<^Ys zF+_z##xcY&QznsFh=}|gG2Enoj)*#r@G3-%FcF0a?-Ph@iD=_-9FZdta~v_!WJ$yp zA$(6DVodZ2gx^U-p2R5QQ-sKuh%Z8nHn|cBrw{=r5o1i;NkrgjM4`l3<9`ZKB$0Fq zG0qf7B!7VjK8=VoiKh`EXAmV46HL$-h*F8vFA(vjSR(B#BJ2!evPn6E2tS7?mzZKg z&mt-$GR`8VnKFsY^N7fEi0LN%93rY1;dLJ2HWB9$-d`fJB@&HCF(OAIrWi5HWJ$z+ zh4B3nkz}I3MEG4mMLgQb8D3VAj zK`b%_63O2ng1<(jn8dFUA*F~CiKQm!8$_u@>Nki~Q!J78Eh4NGVN6OXBK$iL|Idgb ziKL$qn@xd4@-K+sGDMb1EJK7`LX=2sGeN%~N+nW%L1dd^iL_r4VV4lwP0A%icsZh6 zVuuO+6;UCP@hjp3Qznu58zQnCvD2iNBcgssc>RXhZ6ba{cvm2@C320&?}!|UnBNh5 zOqN9KWrS}9BF{uuApHJ7>s67iQ2`%SJy!WBfoABck{?hi!ZpNK+BaPB5}+F{e>u%Nc{^@Xo@A$t|G$zMw~Dye^(ol z8t%z(X#S)Ly~=zA^BGr}KW)mGH<=!u^-?1}dHIo=?#WnGW4V_H^XE*22g2JEku6bd zJgOmbBx0%|zA{-7vDFd2o`@0??TPU7LgYz&V|=P3@+IP{Bfd4c5(zaB0bYpjO`I1Z zuqL8V;-c}dfhdwls)6{4-!nucdn1BtBFao+O+-j7M2W;D6XcC3l}PnQl$&CSwAzTU zT8Q6GN-acq9YndrWfNK(Q6Z608*#;yNo3YVMAkw4Wzy>)qUs^M>LRY1h`I>x`iN{q zH7++*tLMq951E*H7*9{LRVKCp#IBEuKa%#=xFHbF!-Mzk>LjS*3P2(KoHmL{SJ z!n-LVTf*OX_#tv6V*C)TO_oILO$gtnhyWAa6yetlktfm4_}qlZmx#X!(ca`rBs50^ zG(+5G;+i1>TObN0IvD@vh$4xk=7^4_KqC2OL~sj4kV$NT2x*BZk?3TCZbpjM7Bhz@o0_6k%(!H=wY%XVgnGqZ4hB5x(&jwEh0~%r|}6uvzZ%0Jk zf$-{p7-1qhAiO&wvL&L8#~p|qiI_VOBTbe>?41bTj))i&-4WpzgvgT^Wqj^LyAP2kG1vHXMdVAw zcSX!Ixe^K85CQig=9{?t5P|n23MCdA|89sPiKK3bMW#R^xjQ2Geng5%ydM$r0HQ=< zsR`4PYd z*k*!yBT6Mwdn2+$`=oXrdT3v2qJ7SB~F->!HDoEM7hLC6FLM@A(1fzaoUtgWDZ3{Mj_6a z^e9BsV+gOIh;t@lD8hRfB3q)^csz#4k%)N=@s-JvhTrQ-}(QjHeJ+OqoRH(}>8C zh`&twNJLZ&!s}_oRTJ?v!uuISHlmtGbyF=yuRvsCV)P26y4fldI|}3b43)f0^fOfQ zi$&x~)HFV$5cv}EqY$-Bu0+CUL_jQ}j){v!1U`!>l&EL?MV*{J%?ywQl3MEKaVJvXlg>oA}S;@#v+=T zGKtJ_h{)#=Elm3Jh^QA3UgHogO~g2acN`*H!ryqjfXI=Ec>&SdWJ$!1NBG7e0!(xq z!fyg1Pokai8IQ=9h#!w=Z*nCPCL#hRAZ{~p6A*#%h(d`D#(yHBNFr$>qN6F0NS=fU zjzCj9fT)njNI-No zWfGZF5s_06-AwuvMAS5d*HlDz6EPLx{URb;BGh?OnylkyTGd^Vz7VyFq7g{Y9on1vW- z$|N$A5RtPH!%g~ZMARIFR}x}`iAX|t&qZWQL>rGeh#ZNSIf#)aOCmNI;X4-*W1{CG z{N^F@Bt{vZWJJD1d@^FR$(2ZW84)lKF~-ErLj=x86iSRW{x2hnB$8f6j57ri$qNv{ z^AT|-aXunsA)-WLf(crHD3wTEfQUE65^1j>!WJSXo0Nr!@I{Dni76)Z6-0$Z#w&l(mTP3`Dua4imZ#Q6Z7B4)K90 zlgNA%5t)J5Y0@(gQR@+2Zz6V^h&K`5nTTwOT;s7Gks}ea9kMN_JqVSU`Q25!j-%cnqi3-1%lM0tiP!8c&Gf$!16f68@y6hnQ zZc+#)`~!x&dAq2BGJeM?Lm}Er0zlZ znqrBxJVe+hh$bfG6GZr4M7cy$6PkypkjThGG&5xqnfnltdl4;6`d&m-KEi7sqNR!0 zhw$Ey$d>Ro9{GqIiI{vuYm+4rdjR3P9}!@p_apocBJw2K8J`1)e2Mr2i1sE|BH>d+ zz(K@qChj02@DQR5FtkpB@&%X&|yTWMCxHg zuql>EJBkQ9g6M2gjv&Gd5akkGOz2TWg+#_tL|0QLk@*=SvH;P|q!%Efjv>50Lv%M0 zpCPtt#gDJ6Rq%!*`YAX_?-7#y?x77&lf$G45{uN z$oGEuFN$ZHnSXm$NLQCOAAafS?bV0>rN5y`yx`egs_s{)DE{~5dfUsIRv+e3``HN{ zrbLf@&h>-e_8AvEzpU2u5C6*l5NW2qrXM|W-1z5-l}>FeqK$etx2gP}_h$wY8*VSY z==n`GGc$=Fa=Y_;Pd~4@9sg;(UIjOI^B=CytbaE(wo|`ZWxpQy-qX9<+>YDNUi55S zUA=ny7kV|L7ZtUV?NSpjdEV)@v#(WyQ1AWf8SddVeW03RrTeNcJvulLzx{zfJaa00 zP~C+5MXga2Y^}8{_5W|%Kwq?7&yJorTDQTki+8!|{4+Lp23DVF7`z2kYo0Ns@_$mF zIr6#DcRWjjs#as_wW0ZEm~YqS)T=&&R%rR`EPfnET{~6{UAa-!X7tilz9vy`*P_ZF zhn*Sq8vlu@v!T^$Of|Q9SHIVL%c?5-P~8}yMymG7d|11>ceU@=8+OzkYVf&nqsEM$ zIC09ROd8aRzp5PGAL>^3_FA{a4tY(FW%a99_ey$?9|qA%uc+p;6JjQeWQ*Q>pDmJ| zrj=>GG_Bl!&NxjN_k1wLmh7Z4r^n4Fesry;RflFw?zKIwp*gBG9LlQvJ&BpA{8z?x zovQUF#*Cj3GoIf^+h_OUHf~k4>vT;o*P#sm zOFX@|{~A!8`w*^l^U%+3%&dFAhs#gHq(^DJ|JJI!;~C)#{^4^1|h zL=`!sVc$NnOZ6zPYIRqd!}nL8R4rwddANJ^Nln|W_Nd$m8mpKIPsNXZ?rGO(Tpd+B zQaLLBLth&*zsiOGwG$X5)w!aTasF`*vF-2lu0H)+9r>SZf3k1&_N{6!j?(`j&abk} zHBUV1bwB3^elFzdwMqF}4hYULm5@HeETbH^q;7HR_gW7ilHz zgUV^6ZBqY@{0-K8aPf9|NL+D z@7i*lxLq%SrflobK&D+kr2%PQQ>{Xm`+Y>zd-euIXx$lIBosv{YlqVhBx%&X!)YCX(7u*_P>4yt ziL1)(;YT|P7jZh|Z-rm2({JLc+-;C)U72;a<9cZ!lYTQ-X$R0xA8Wc~-5unM$^D7H zI=?8*Gk)bUpwal<7QB;OzizHAs=%q&K~U2k3(oKE-i142JEPy=)rvd83EP=Jt>YEM zKR*TbmvzC~{w=oh-&Wp(+iKla>pJ7MS*HU*ZHK^kyAluUy5JJ5t7hH3xMj4X(eT7+ zRJ($H4BD;Xs9_7@l_uwxgzqOm#Y(i5wQOA-Z2i0bMfF&;z?w9@%G;vTq1t-uOpPD9zmy1wL}>lDf4XI($? zQX1B%HMQ;$^4Z#*+W-1JWp%bc%(eU2`Ay{kxJlMIzpEU9bAD1!|3{xXt;c^9YAlPU}@D?0>t^8TvR9p}8wz$e4w2EYAjvcP{w!y);XK@b^Z?z2$A%DS^>tM@8 z;U-vjhjl}7@zy!Nz5Ez%l6Ct1Wp#R(w*OWugRFd<1t~1m{?ZRGtKe{WRS8okoT@wl zbFB-uUqMw33l9!VZ5|TkdJx zW`v~xCEH2MF z=U1Y~;PS20FGZ`H&%vkGIlmG;7I(Dr7m!GtpNxK>ymK4TlWl3dvOk2wk}pDSjoBYhn3G#P`fc1{Ury4CU0d&Yn-h+AJ@jZ@wghyFMt5+5^UXt+Wsb1POdZyY`Er$Xl#@8L8o40j;fDXe+P8 zsc{{QuMoB3RiK76Ew=7;ayV zWmuO*u8wJX)4HwX>X@eW)@>tqt7n=rt$deUJ=3(ox@>ZFOw(IL?UMIE9ntKD6#*a&3X8o!0Fk*9K_XW!)#_ z+5k6r)|7-ff%2VX|w%{4-PUAFmn$BAH1^EH%&RKT`chI`?)}6(DYF)8) z=WrTzOK>@OS8AoriT5xJPiBs#$kg$6th%o>u;W8*E*5>#pEpY=d4nZQY;H5vNx+ z-q!s^9%Sp*vF>kNGu%RAecRqu@;bQ6iQ9FI#WaIJa8MV^RccP?rrOCz$x{_ zy=&c#wp?|bJ{i)h9bcR}?S*^Fj;db`*3Z1F27h{6+0-^z6W7nWX0}0ZT&Q);t*eFW zXk82IYUA#*PX9GFv#vV)x!t;!IQ6xzo`2h0>2GB{3Z|03MuFDW)yFNfE`X>lX@Jww zyplW+r%`W+``NnNam>2(+j8|;$13s;IR3eAz*WPgYy3M|*$DZomNIoAs`8Ee`JP-) z!S~v7zPKN4xd&{y#<*f~JuQb4wRKH!hsgCT+Ji{HTz3S znVz{u5H*CYD7c7R&s-yk{B!x^Qmh+I)Jkr_%_i5g*%+eg>f^}g$@Szjj;Iyuk8nH&JmTze>_qRwngemi&S(Yp4y9=6u$w8WZewwZo}!Fz0Q7aIqm=35kG4znP%F89dKRgjLvF_)^Ux))y=w>tm}wtgVR}U zmUa3KzW|&jjS#c0ApR6{!0AMnWZhjjxAUJw&q1nAC*)rHh?s8+-i_;T8(d&rFfPKn zh1T7J8))4t)^)}`YMpi~v#t>SL|$`l*J7kLz6)}&Ex6Pcycajrx>v31ihImDJxrN( z-N&CX*6Cp?$8{gqbJiK0*1*hhJ%kI!>Gb@DbzEk0b=LF0myPQSaQQ}KG7_Lx3ijt?Ps9ORh7bdkYEwTz#48uY^gD zXJ%df_%pz|?L>9v5nOBXy2KsU^(Sv*-3LUr$u$6{<40%Hk8HUJaveW%A73lyb`4}k z2cJ%{`-lYBApXp=E}zIh*P}SSB-3g4FhZS)B-iO!r&yi92(H2WX=|NMU@A8R*WS8g z))B3DN1(!J{^*%c&lMgxt@v5;JFN4x<;LK0^!%?Ukm|PJbL2Xx1cn7P-w z+2otaHFd3NU;iZ`bCG)Txz84yLq3ySPZr&X>h)am3FO^~p|)Hy`6TNeB&vh+aHGlZ zCq87|%jCK;sOe$r=IiIAb4qJt>SNs^ z+(2@jiu+o(m|Vw)PP=+ROQ^hppyQ*D_WvVDwX=j=2Z#y|ux=@NBu=MiHLP;4k`Kc5 zBR*NGF2f(KSnhG0TMec$qk=j=KVb_lw*}=!;8gB4a-EO{ z5~FRo738X`Q?_2$6I?6#qm{^wv~^dJYvbf%Y`NF<`cDUv&g0M6f~(0xaD$1lw%{6a zmD4#}`&FGux8-!o9%IYBL0*$`nx3<6ElzcH${uU$t|Qkv9@qXKXA5Q^wIxpwU$E{? za&3uRoUOc`Tw5ZigHfHyB-bf=1To&a4dmZiH`%(ka5`go+9dzp%uSFt}W5|{v}&?Gr8K+seP7pTgcU(+-zGni`=a(k(^`Y zR&qVwb-quwZX3B)tn>KG*1b!vmFSec07r0T^G7R@TWHI@N3QmC8ee4Hc5<~hPWwN_ z${eJ&MCa_Kw!t0bI^f0=U$yRia=oyaK-AlFg6jkRs53e}FSG7La&<<|;51r0$+f|9 z%PY$<{=1k_Z*(4i%~sw`uFmK@zQVeX$kmwy;!5js$<-O1vR7I6u`Q=l_UqQ|vE_8i zUTxhcpbr!zKPSxZa;bD zd7k)|t$Tnxi1reR?u|&j*f_{cPbzDA#}@n)rv`N@);U`Z9wJwRa@%aV!{mBVqSN!c zI5l>%)B^@eZ6eY*b!?xUMTW&e=s4e$}Ew_SLVBHy8ZYA+Ex0Pp^(LQ;dcnqgqa?ZLn#N*bTw@zoZ zlhzfJtJ4|8FK`;_FUi$uoxslG2(GXAqnBYiF`cu{eSsMbjm}Kxtt=teK`2*j8~mDF zE78g7E9<@?*GhDPvokr^Fz5U_cOWben|XHum6ayGXAKt z&IP~Q%D<4SvL2_pI7z6yn5@crm|n5&S8_cR_3-->r{OOr*G(FFsQqor{YI{vG~}*Y z_q!ges;q~a?vPUD3UcidJ*=wXwCFN_v`geXZMi?lRZb5pFI(=4EhksQmiv?ZM^>yy znzt?Y7r9PQWj9tBNpgI+idm)uR()v)FC$Y^GrF2SmtTyyKZZMpNf z7S`3W<>YR*u68x{zk02QP)npbQ-^}uVm(}L!7=NqYn>h~`hZ%l9)C0nm5&zNV11n0 z)1#%GZLa}N8>dIct+rf4oH|{q{ePR4KDM$RF8o$gWoK@%mG#KbwSI!Dk*zGJYyEOJ z+PZqQ=vu!v#MjouOPr{jVX^(H&|F-fRo%=+?(W(dKaS*v0lzZH| zyU5i+9m&J3>qM@)I%uD;?rw6`mD6oWYA={vb#)*=Y27{Ks>?y^c12s+8L7%TlAp3J zgj|(%B#*SN3%M%mpncl9d&yN-F2=g9$G2AwC)k>w7aHT*WWtrh8fqK+ckh0^;$#jwgn@|)oZz# z)(y1fv>Otw8)VDLy=2{^UCHH>F@?LU{wvl2W|k}1+Saln}AMU>VGTxu6@}UxvqP@gUV5?oYr7hyvZ+ zejHB1Y0#bRx|97ZoP+aF44v2+!Eg_Bh7hR+POHkElOdsy+*UNQhyS`4Lo7pqqO;`{5j)ZPze+%^8 zh)tj`J?ImN`)H>dxbNqWu6gS!&)0Asis20C76aY$pnDuXgJYokCl0}WH~>4~UC>p; zt&jy<;4Rn)n;-+;gblD3HnUr|Kvo_0|5lQ1@Gb<=nNBdB6QDn_HMD`+P!qgBcM|B% zfgP0p5O%^Y*bN^+E_@7`pxg1w;1{?A`kAd2unJbgn%d^nT(7&#nq^*rjo(15g#xI} z@YaL+9;SD)*G=vT92XNI9wxzLNPsCY6{f+9FdD|dQ!o-@z#qE8eb5%T_rLO1|DQO9 zeugsm1uns_@C~emb&vt;^-@;%{BM9aAcl^uAwELv4+9_q2ErhC6e3|T^n`He1$Q#^ zK@dRR7FvNn+ydeFG0+t{LkQdho-C`bml9qiwIH7ZXg}!71uNhK(3eg0<&tfnZ^mSU zzWJhWy37DQ6D7f1&{aa+{%`@lgi~-5bia-6!+9TcVOSS}b;VhCt8Lb!WDLi|I6X14 zU#me)@P=AY2kJsSxB(i0FSLN0p(V5ef4BwOKmfFb+u(NS0Czw~xD$flF6adAyZO@{ zbbHEP;%(d!Z{7z%kHQG)_VzDtBW9?}tYq z0tUh$=m>X05ZndLp(YeEY9E4rwsJ4z!vQ^mZY9|U??N^-gd5;S&>O!J(A!qM?bW9d zhaeC3!Y82DS9(3A*H3yGlnnFWWta~OU?J$$s9t^Qg{58+KM%w7!cOld^zLB+jSVL1 zyC3_>^?Aq=^0_b%UWQ>XhoPMd$?!6ahUegU7zZ!Fc$feaAs!}yUM}mk^{t>c1O>G3 z{)|71wIX;G=D=J?hK_It>}7YHX4t=gGjJ9jXGn*`2zU~rAqsAQlZ@Oc_yW$rc_@ZA zY3n^=V`u_?wOkLna*!XwPSA_^yXe^n;u#)(=V23Dyalr0J=hL8umkkfjSu*5>b;t| zKOxmuF80Dc&{r+?!vQ!5`kKWdI1ERCuUNR&K?b}Di(oP6mGc77H!GSj-x%saeW(Fb z@h`%3m;pE8n(6Br`r3uQW}&xL4WJ?D{oWsKg;-{5!9d;O1K5j@3!g>qtEPo4=I z;4QeDx_XZ|lCjX+IlX-w1$rm8hSxxPqgzPS7b1@8a>-{TdW)?)ClX;cbY%Wc=mdAe zk5tg-g8Cfr66mwP-#{PjRlsG?M|%1QPaoO+1+8e;AN0L}0B8$=&>r*^fkHO=guaiY zHxqjEp!W)TZ!i=dgJJME42LIR1f;+cSPIL)KpNDdoo>((8Z&5o3`xj%H10Vth!XVJSlS5z_JPuF7NC*Pm^H>+W;3)=tBtbjfKiaH){<)yp2bQhOy+R{y0x+zOH zVdg#Dz_dbnc=)Qv=;V1YR%HS8c1iwN# z{06^61zd)2pa^ndD4l%_hN;sK0s0zm7tJ$@1Hc1HK=(}F$EaNKWdDaT^8j3Bc-j+h zgF8SM)YgM8qHP3S56c2wp1PUkHQ^gp&bcz$IzDQmI$&dsm*opx>i$%adxF33O z3V8@N(_Sg_w^CoLZKhT$)^}Y7pFdNh_5u;}rr_*=RpeJ}k z9r%rgvtTq0={v*v;Ajcx!=jJi8TMK%)T8WYl+%@mkBK|seb^4K!Hf8+b@hSKtI&&j zrFtd(7=?y{J}ccogZfhHv+T~>;C5&SDKwGntN?w2^(35Qc`mUQ3Vx?6Z zzK7+yxL80Oz_R`j1iGyDGw71ran|Qv!=J^hd&_Ndk14(qp zsqQdc1_8|fLLcr~|dY3#x%9{7l)aZ~}J1Huwv!z-3quYd|*+>n7vz5DU7~ zxflEn<+>?AH|Tv2Kfrk?hO_VmoCMv*_$eHM!%zVG;Q;J|PeAur?uPdv8@9kE*Z^U_8Kk$Vc;4em(?`gOzztr(NGrAvJ`@Jd;a6I&Yhd>WC?dP93a1?bfZa26e ze3;j*(Z%eR3$O=tLqIE>(0`Cnm#BMxb+7M#JyO{JRU5jSLb`dkGiW8c7xyy`sEcqM zPQY%c`uKjD6%BzX(0>)tgV+r^Lm#%ZFX%SfHZUI+z#K@1dD{Q_>e6Y@H88;7pfC35i#*QJUq*gAz60C=Ij{r% zt=xa=n6`8-RBkQtx^nJ+w#+H$M5n^Ptt^y=bob%~xPJ6>{OE#U7vi0i(^=BRh(65x zgP`L zm(_9epO(3`PzT5Lm7D`kUxPab8p;sp0#0N9SNEUVF4avT-_qcBRT{m%usZl}E4sd% z(_yD`*B=*pR5{Dk-nE?le|w@zr}=EL=EIGwq^j#J)=l{ba`+vEJ-I`D6g z1ZVf?3g+FQ15UihF{60>!&&D5XB5;4w}k$89Q|)NdUf+P2VDuAhCwuP{T4K=(ty*@ z^;>`aF1x;sD)YPzph2hiztDlIdEM)lRjK&TRydvLKv|uns&?l3j#O>?T3hV@sz>zo z!&!BO*Dq6NwQ^CloU=hym;a}7XZ6vB9$x>pzpK7#4gU{is&1{GC$z;{Swr}@Wk1sC zpFpo4D{l&lWFP;}VYxOcRsHq3wytUi|5IMIo<^zaNU_XadDLFtSk+2Z8~)$?zqKLf z>^A46yEA{SBX<8+?UgDGW>o$!E3R6mYVNG;`dGD0<;Ljw|37V&Q@LuT|36Oqyz07? zyLHmg7XNPzJIkD?h8!GMwUbqIXSvG%OYW|0=wBB6x8bka;PnkT9jWT8wpVre|18t$ zzqX`mLFd01c=5C@`nPrKD(Wa6g3mxS+|B&s#9EB~*&+n8w&ZNVRI1iiTN zfqKBZdi~Fjde>I@-}CVz$i;E&Vh_9MmtF+9crD_x|LfmXE#qBy<$s!1pV$x@!42TF z)snmg>#zL3=Wb#~J@SPn(A3T=_p?rUb7%&td^5Cy0B8-jKpRjw%`1vl5D4wSS%>D8 z-`cf`RO9N1K5)I5cYy;nQ;)lH;nuv`kpgZA7qNQFi43e1O> zVHV7UsW1^#J`P@h=iylx4Wr;0h=C{=2oca99szy+(N`VmP0|Z`!o#3#3WHF10Ce%L zGu#6q(9N7#@72WJjr2av!h`S-^nh^a0|P)!KMMN1B@zb15Eu$i!&C4W41;KR5=MYl zJsci~Cv2`b5@KPjj=V7>+Sun{9B6^iMo)k#Fd5=u5+uNMmAA=HoOIIfYy-?Yt(b~PKUz=kX#EHunyjY^^gf_cq2F~)Z;9R`E9TjZie?^ z2V_GIY=`%t8hi-*U>EF!z2N?cKf56h_CPLt44=U5)p^1PolW#}J3gD)I{1K^wtJ|#a0>ZHnO`8n;m( z4J$fjRle#n%^wG6x$;w>b)AJX+W%jWIE`uL>X|d|M7>Y{+=C8)E&|>Ux)Or`w=w%lbydx z{MqJZMAiQtegkLge$YG(%F)Zh?!3Zmh0 z7zU5QP>6&m7z~fvd6j@MYQ-0SOe2w5ln>zFc)3{H9ZSnf*CL!rhqPRsD1)W z2Gv(xEmKUixtpkRGqqRMfC?-GXC>;)e0Uk=feI%>5~v|{WDcmqs;do1B`$%*kOE8L zRhY=&ttP$>t6(L(25DeCLatrRSkBA}NC!Q6>WOtrm3%Y#JFp4fhK(L(YL?e6ZVkAe zfc1o&O|H7{61TxtSjYT(L^dddJ#}|md5X(SUl=Z>7t}b5$ zUGb?&JHJC29EU=v?5Uf-dUXDb_z^C`3HTn0;T)WTA~*@B;VhhiFF@`*T!3%k8z_M< z;XA16)XrC+vMQrxZdE7+HS{%T-f2t~egJjoCr}x6N}ag`+WKFKzt~((FG=KoBQFQJ ztFR9DH?czVOn(utX#f8~auZyJR?yPU>nZ6^@~Tfs)fh?-aM`FUerFis)+l{9WFfik ztC-5XRy34c_gmB_S6SVE!Q-X!J}2Fe@r^EB>*fr7>)|cpF=7dEB2hPPd*Ttl<#RX&y2VJh8tIm!?VwwbvY`{@g1`&3wR*Zzr*tJS0CW%BEl@+f(w!{6&(c>r!|V)T#NPmA?#5=U*Z3tb*!|4!Fq>1`k0lE7I2WAb$`-LHq6jVs~&JMmo5v zc32%%XSB6dS2~kf}?zXm}E;F4u;Qf@h$g_P=_f;ZS8U9^zmOj0QFEEW7~Y;CXlsbmAIIR5_>f&atA= zbc1%OMs*Ts7si8@Im;)ikP2vF0;s?g(EFhoL_PGT6JLaBFcrL6vGOJ?Q$vX`M-iVy zdWJ3kd;x^RMgX|w(cZ+58s07d=eQ+;?z&#KQ8o9e5 zNYDQrN$!9SprOVDMZ`I-UVZH&>hpNNu$$T~N02gS;e}W(3 zBK)8s{Sr=rPBa?!(@+fO;T(JcXW%Sp>4JDx0iP}ZlHL5!r zv>vsgb-LA%3P?B|(7p_ZXJ}ZLMJGE~5Ca0z~aa`+W~2bK8~{)WGx z7uYuWOys|2{>dHppJEM$Qp?n6P4Ze$7wUjF$U9F|%H>sNEO~9>K%$;^#_I8}9;wh! z8HD=tMmfo&5Uf91p4+Xv^J7Y!4by zZMEt+8|FlLrw--*YyYdF(~wi(`jzVZaJ_Tpb^dU2Md#7sbVO}d9W|YRMri+Qfm5-I zZN%xVmg#KeEO%nnt^ZF>2cpwiX2GpE|9>@XWppL`O_$3h&80=+a-nHQ{&)${Ub zcov?6G0=u{_Bi76FcBuecpKx1Y9N#q=rdHk5P1Ub-Id>X8T6`(d=BQA$!;FNon ze5s!QmyqZMqAHDG=nPQ>Q(-ksXLqEs+yh(|&A-927f@QkTJjp?npYVu%Yet2RY$ck z?-Dn|JD`nt3o76LZ?Iw$QJ+q1Bg0L}u6An^^%;ZPpRGGeg+uTu>;@gd2Z=R^2Z+01Cwu^zpbo2% z4~Z({1^R5KF>$|L=Byx}`4)J!8%W$o+zYlY-|%B5hy{0n8v2NNHLeP6G!I(IC(P%; z9{3n?K~6Yzv{Kbkd2PfImK_G=pAie7xjN2#YvNh$|7FZ*ShP>iQQ$b7g{uA(^CzGP zPJ%kP2G)Y=Y6V~5-zU~0o`y3}wGFlDtViqcrfhfZqpz5`0D3?7J@H9~ayYpYr%*;O zIK*!(FNa@29j+k$4z;QCH}Nm{6Rv;`=0Av+t#fk6>yhR5)F+u$rZNgw$LWsUnxL0a zHHej;ShDOA`~o$ptWPtwVs%WhAsyOHyvlqGsEjVy@f8(UJ#fB7^rgx(PdRsGAuW(p zMO9F)5l{m{mGzNpL-M*%1H8Z!bh+^cma*>2+dx~9>&s^?;bv$=8GR8=x1{KrYiqa# z^fB*Y*6(V;0^Rto)8|b@KR7_H&+M97uS-puSDC7LbCzotR&{F2nOA25p&jU$(YNCQ z;AZkR#NBR&vNg#q;14ZXpu)OCe;{ZJdcjS29kn+Sb?1IhcnEsHgWz1gb~>)h(rT+S z%R(Ut?gZWCud>=0EqANH-B7i_9n5#Ig_==Fg|!u(;4V-j+5*krW9L;T7)H@aHSCmE zyZ1sD(AiO6WeTN?%2Lkl>Pm6}@c=P}0^NwZ@_!#O0>1~kliv?op|((?69&$xI9uJy zE~`3fy(y3|y3Z3s>ys#MjDVE%E?saIdz8w~n5<}u@+_!fMCJSh;`1;To`W%nwMZ zFM^jK5oYL{vC~P^m>SYnCBTa?4W_~rn8>iIj8?9S%GKCRo9n8-&Xmc_&jpp64Rb){ z)nTnC3AACepmLpRL};a|;B-XuPA;#`sZqsOVB!Dk>^tD1Jf6RgQ|>INh@gm%h+R>U zdVnIL!H$Ssqat=uR78V{1c@!iijA=#qOrw_4eVlKZ^06~1WT-lEq2BCJI_83hsc@V z|Mip2bI-Fgv$M0av$L}WBra$s(hMZE!0Gt?3zAw`ZbA5_mcy_r?FlNk>WBRAd4ZX! zyzea51f;o0!%+L7NK5g15fUNILt+Uq)2w(b9mb!Jq?9*7RwgFg4JjUp0lEDaAgN5L z`K+v*$1t3ju!^$;zZWCruYiin@c9Zy*=YZ~0C@@I;ZlZSW!j9i5@`j}G9*>8lJI*u z(j251Bv!;kB+loa!HlcQ&2$)7{>E*h64`{`YUWz}UWN245-TN3hilG+*C4G{-dO?p z_eLb9wI1m=q;*Iel-~sv&eGV5G$9%P;}i*yTVC3Gc|`~UBFIe~NxsS6TYFx4OV1Mli@ezM1N z6yJ^@9Y*3n^=c3@wRn9TFdXw?*uRl7kZvN~K)Q}}tuPHqH*Z?v4Sv5ydWG~a?M^pu zGvOV6#)7nmn#?ieAo0P|?~wTTOA{mm(qO>2@;jb`Zf8KNo3!ktW(-EvPawxE(o@f8U2D-$UZ?vWmbpRW(|z zbwTf($7cfh@H*=N_Yjo^SA@?D$_!6HVR;E~ZMco|0yBd(@$EjIoX^UcS20;B`S2kt z8Pj4I7O@w8b1%tHOQpew$|h99|5$u}c(Pa4&=-I2N?F)=QrH4wQV zwt8KVxJ}vG@nr{0lrKBzg!Db8Ivtf~-dZzV4%+aO=`q8%^0q&>Gb;|)x;HW>0yq|_ z7v6cviboKR5b}~m2*V%+Bk{ZXoquypHz5r`USA}_Z+6-OkyvVeNGyRRNP+J7A9K$l z>JQ+4NQ}G)X)%&2D*nyHm=Gf}4m;^Q15Cb-4E*a-F}2HOb6sJ>sL(M3_``#Trv`S& zsJBvU7VPTe=Hx0uZ8oUZyrZ-2PW7t2O3rcNoX6Vu>N=Z_E^r_sLhB6~1)VPud$ebr z=~DsY?&R)-91hqO1@@?=hss$+o_>KG7bj;YZ`5`4-~kc9vFuTK=oHT$S(awzkenr8 zTNfgWf6Z;B5WJVb`=kS1sDI_#nu7KuTbR?_aS%j`s4%SZd;0M`p#H3v@3+4u6|lQ`&WNFKw+NboM4H zJgx}E-ZU>ujyKFD^`m5t2H-Y$ux@&(OP6%vTC-Q6%92*lB-5p&6{X}G=B`?BdKXXW zLop12z_eJOp5CBv!eH95>@&J2Tik|%q85Ai^ z)iTY;YVAvt!!2_MI*@4|qP}R)l%}k*dRy(2(iE2s*3GTRp&Z_8TT$FCbN^y3!5WKv zi4|qsLX+>pI}^V(?fU4gfhUWA2X`j}^Kedq91!zxOhorvYmwuMdbq;&SW(^E=opUy zf#^qUqrmWS6{q~}0|+z%s_LRELm`0BRx3kMw;?0V3z&y*8pu{0`*(REAF9K{e-=iNA|SLK%8sqpxocRa;$#2J@%nGL*p217#>3 zPw5Ucft7RkJMXt`hjgf*)B&76EkpMIpq#g5NP<)(J)%RS8#1^1Yf#W*6d)^`O(lGBttptA-1uUJ*6GJv zD6n8Ape7}Pnzl_X%6e=bV6dny`p%603%2gNcH(cX858C4OhOK8;LrNniEV$~#(f3d zfQOgCwP|=3%3JA3)3QJdV%^8@0p;jYmiZFxfjSiNM0}Su^<*7NdjcI9T36IyL*;HO z-%t5mO{OTf=UC)$Sy^xU-t1kbh&ys%_gHwkdQ_78Jj7iq+uX6(oqD2wsz`UZ!n5Y3acR+f%eiJ=gf> zV0wZp**yn>mm3B0yUMg+(w3GyH+R=I_n>sn?CL>}o2-w zi?Uxp?_9mf=Ot)0MSJo9HfM2*!7nPTuMb*^}Bs zGrB+Bd@1$~m<{!%C2t_77$17@22BXZNqP%;734U{J_9>WK@71PWw0*)aI)vyitEM} zW7+~@t1sO!)7etOTQvKAUpoI51;C$jc!$oxOKt2jrF&R+>VD&2y|tNdBX(3=<)!K` zeKjbdbZXK8tTRU?Gtj?q;Bu*wC?2k8E~q+g?ekseMfaaqQaiv zQS*=Hjz!vhhgm8h$>IY`#FV*}fSK&H?Fd%_$)V26kTyp}BLTJGI(Zra_>QnQhI5Qe5H&)n5vty-mC(r6`}~WX{P0T~D;w0t;qr!FJ-qDRc*EZUZ7*mTT?&^6Cf6wrVaT4LR+d01p`Z} z)JAmhx|h=~YKGQhUqumcNFkcdkczVZcRy1hwMrQV)3iUKRJN#t4pm(kdHrG)lVJlOtaTO+0YcNcczxI&ZEUs4diy8t9-i zB|6e@9SV{!1mU}_Pr=8#Noh4Vxz(^_L?|C5)UYR|7f0*I0D@hJoDF?m6=_zv zAt2a}Kt0R%B6|z4SfeL-S%5{nhw-y{FeUMKj9_~#bOR+%j4e#zaEjH*{Uf3e8}<8X zFoo9yWTD=aSqjf`y(zo|o-Vz~$r8`Dy~(c@o}saXlEEayRj=hql68;6izNwjoZ(BOe*QknYG4;=5w7K%G5 z%=9|o@@P4<#ID|9nfrB04mvjYnO%}=)`xd1^TMH5#-CB73C-Fxkg}N8EA3*$z9Cl+bUtaY`*Nr918>tr>M8korEkB5! z;VD%gB&z#9D`)uY4TnwulAUAFXfTNOF@!%L*vs*Cyj>=1-FthP4;OS^jY`9GU2ca` zHEXm<_aW4cpBN-&TBG!VLud`&4F`q_eaw7VHtX`bQz)I)5ru2Mo}G9i#|=3rhEhp1 zoZ&KXxsK~AefsfQtCrx46&30z?>J}4{k?h1@QxPSZiyToG42hep1_sf0GA=Q9yxQa zR#fCB0m0PssfAP|Oy0~LJ0zj)WKY-Iu}Udm5qg)wd^iAtBRQobV(PrPTWGeCkA}$M z+0mnWLm~<?R946}0rF=@XMb3tLN_(L)O2ku&1q5^4 zEY7|~=%{3N2o+*;hmkX4q1xYu(Z@WoQLXx2iIwJ$ow60#Rt`4dYn@fO%%||G+37HX z0=a`KCPO+hLiF@&mr6#h>~b_3tnokt=KmZ)2@H8<1f|&OmhjZLLwTryPdNEiz=UpT zd7Y=!He5)!Sk365hqh~8SaZ$bb|B^#PTA!l;Rrxjq3+V5g~_cKSpUHg?AEzzCL@P+ zVA#)-G~QQy~b+RfN}HMbPXD5K7Te^m_&PDrH8IqlEV= zqbOJch5DmXH%dB#nfCT*x?2$$)qfycdt|hWq=EK8=oU$5?R5`wrFy24uAu}aU9ue!|!Xzb>eh^U@6v9GwD$I|Nr8^$&^( z+jnE@c9=pr?tpGGK8p4MSDFG`whgP}6SK@6LYe@A9Y)~Ji=t5lN!88wa2>nBwb0H2GSY%*~15p%F8P%HfBc(BfZ0xeJZ&IGj5L0z|akzOyQ59Dv^`Pxf*1ONK#BSa2!2FSdsoSQ@VuSNX&*z z*;|41BPWYq5UuOAw9(Af!;zzC0Y{;LD+Nv!e)x+1NBc~z^+v>3WnGsSk#ZMhq+|eE zqH0V2x!6J1J#;xpDHcUtzVZ2)(yK!$RMVuqONV|Ion+{=E9aXY{w!8X$6rPfy#0kTvGPP&HFS307?~zd7m^x0 zPTP3*xm&$B;vkzV&AjR4!~R;nF(lW2`@)`gdq=+HkJE z9=%naQADW~njy8BDP+ES-Nd5<|LFM0s1a(rn$nC}eJS1%3Yy{l*%VL*;>iI7yT|WJ%xGGs)9fS;1i4}$KqsClB z&~4m9=xiO7idc(O7vH_94s{P=;wtOsf`9h%+dvC4f<;_*F^yc%=q! zbme$qOjO8-uW(4wF0cZtTZ3#_v)A?&pE}_$Ix=sbp_2pHEfgQ<*V<9Cv z3gf#abi-e#%N*c!aUL5yFdtN+B?ZO{=5^1t%V@R-o{h`UI{u!#jBa@7c1Vwcg#XgDLxLvf>3hfqZN$?P z4KRY5qh|c4GdU8{;m(?&3;J&9qBZ-pOmt+dOJ*8CvtlBZTm?wiM4`Z+Cv-S4;Hpyj)E2oU0&*=ls2Or7r|^Dh+=@#r0WEXBoFEAlOSf zcI}>LC%0``Mu-UH@a*T8A8*)|EA?XuW6qrABzc3gO@OGxxUCO+Z9OtXi*jAP5hHNZ zoL){rfG|7;L=`}sY2)DRb!pu=BaISC*jRxaE5ieaiI?K~VQ%FHRB!B&Nuoq=7)z|X z`7}W(CjnUskONjt>e6oUY|P-~2p(b{SCc3l5Hz)cE=AjR1^G5aktX66P{=MTgmM&f zvYybgRY#uZAQH=S(mpF_W<#hXoZd91F&<7XyRNmKXFMHM?pZ1zl?7$fF7@^(a`x5P zn1h_+GfQhhhIK8nt82ws(SfYl%Is!J6!qlDIDFt!(1`2tedSc$KNsWUBV7iIb91*?^_}^DLP5Jm}n}63)LStQkA?i2LcZ+_WyF6s{ zEi;8r3|Eto!$NM-yK#%c8n7I#Qn2?!?U!M^Wr*+160SFYvpgIS%3Py>Rd8jU zpvig)1FrO>lNiW1j&Ru#mfes0rs63NuR%!+vA>3(ey-d4^j7~a|A%MyB0|w%BVF{v z#O#|I$ZmnEfl}>F*arws4OgojD`~s5kx`o!9o4qmLBD%oA35!17YVv(=l*ZNuV}wd;?G_sQ9cb^^LK|V9vX1a+|v#mm(8eM#1-hXy|Ig%|# z(!i~Bwi)o1a-|SJ@=VpM^~r44rFLV%DKASwH-~Pe&0SHKv4CLNHyrYA>f#%*1PJyH zFn%i|k71%hV|K4$p@n_!_yR()aI=uZeS6rfA4}eG*yE2Jj_v_hZH>i%U{^Bi;`Z3g zBJ<;ot)a>`c`IeK0v~bE0j4#lQ^Ju!se~;dYP%`2GaLjhmiqXgP7k`W(H~M*AxyWq z(qaXSQY!8lP%RCrHD=taS9Hl4j2miB##Y*gR+nA@f}Ksrn6VbI^_&kFA;g-#ZNXt; zSxYO5Fg4)6~yA>ue|Ff74bGD12 zR(hbTy{1$80%K*q9?0_vLUk~{Qr?1;R#kje08o6tNGP#o8(m>V>#fP59bOvjAm=tP z0zGz!LHK0Z_cp~!J?A;CC$=h}14JT+2jK?0XLR~t(|?=D!5E;Kzk_0c%iSkE0NsV5 zRVgJ0pu3!GgK0pWo$&zxYQB?_+M+!N?-WzTCe|B5?Y49o&-yHSuY#&$(s_TQg0tI! zLSE9^FKQ$h$b|p}7mt`-RJbjsQzq=R3nrv+daO~g*(P$+hNTd52fxF5Zsb^TUBiW)iCC7X>H^E zbfX>S3hnn(l^|UMwxn{KnM6?ZPI`4fEEWY{edZLHWcQ1a!_o&SGYGO$wMnp!0N>F%TtWhN2gJ8dr(I_IU`)>N*~)} zuI774SV6aW{h|&nOTy}x3%;;N-WpHVmB$VH^f}=gSQLbK|A~j2NAMvE=m0`P57EF5 zXyQwUXyX_Nb~K7+i#lS_pQU1^1(2m#CTQ>+B61I%WelZZO^!CIk>P2JJP9#vER}vv z2_3PLq*ysh=NHuys85xaX?vsE3_&J<7hLvrLdAw2p=X^C(HL`td^^Kpe7(|-38lPB z|MmKP4Er_E>MreQCU7Maxad&W6`TeLm2Y~{8I?#lMkTvow!7_^kl>|~-oveI+O$F| zV7%i|dhapniH;@N9Ty|+xLTBZu?YQSVuUv1u zF9wiuXZ6thPn}erIim&ZViZ@pGj~>1%<3SbzANK>t`Wa`l5Tf}Fr!Y9b2npkzu__b zw?AMa+U>5oDW@Ck^4U}5-W~0F;WPzrKqhw4Cw-5n8g7RXOz93c5#a!P z{yzT?rp+QIuv zWYH7vYcG%=Keu0?(LHt89&?c_6H(@mcyfKFT_nd| z$cH--%D{N&zE~GQT!080<@p4hA#vS13Lh%&ojaC;9o{3gz(kxT|E_8%%DzCj7_w zUDlelTy3^b-Z3Zpj}g}ib8cHh0sB>9z0qYfAFhj8Sc^`>lbV_jY02!19PDI03tZLJ zke=NT%`&rahc55yrCepVMa=)9*HQ=|RigpD4&rx3|E0{EICq2QdfsBh!o}6v+(a3Q zuWp$^iGVbCX5bJxAg*qAaZXscfWwRmL?7f}DjTcYFsi4wWst3$gKc-tngtn@1zhPU zAb1AfduNX4(mQ8ujJVkuOvkx9>(dRstny$8DY(0iNtuQirG207J{E z7r1)?Q3nvsZVz{zdDCKpk@`IaxA~qeZ@gX}I%A|-{1(jwuH<%0P+z#tE@`(z6<#1w zIO}$c(wO>KK(J)mU+t6TdD=s8CYPUt9GH{X{!&cXx@}#*GvZ#nMJ4-z`fEVc1jOc- z-RIx%UHiQ#pFO&Yw<$IZP9hAaOS{|DlcC4m7Vb&(0%lR^$!t8Ro1QmX#i; zj1Stf_h=D+S9+vW`JOPC<4=3-nDQ$7vL)9<-c#$J7C#V06zn6)G30+JeHNJMl171m zG(>_F_EN`YC(mskP`LCFStzbJWT4rM9K_9HeZ8lJw0YzU>SPNWtoUMqsPw5PC9uRO5Vs|?(toU@-0?aeE;7e6}DfEEirh|`K56FHH zAlp6=N>It6}yOcaM=(^EU06F>rg)w9zAUP<~^~v06g*vWVY%KNg14?3u zM}TkuM0QZu*$+yGb(A5nQOsRa>>>G00y{y`F(QcloqY0SbLXU(Z?w`Kc zKKidcLMn1j%|~>AaciJMbBAi;GG^I=^h%wKH3$SQ%S$R-yKhG3X~lD9nYYa(i%@Vp z0T51rsC>8RgM04%jvH~uWRf2s44VMK-79TU_>3ceXD%{A{DmAx)fkp`NV$J7Abb~po`dGBf+Ab%OAFmz%%t*b}W3n6q>OU(GgR_@T zYBH%F@ASmXg{f~s4oma?yN^xn1~yVm4CmZv>G)r{+zWNf22heCfD4^gLX#Ak3 zE}EKIv=O*cw_8FVs*b(g1Tzp0cLJAtLz^thWa<+@9ep|WO;E38k2+1_RXBzER^+h0 zPQC7VvR3K88W?lN-Xf==z`Y0vmiJGd_56ZelR5AyQ+L*URB*TCj4s=ybf|?9H|Z8d z16Qi_L>QbyckGikw;8X@(7E1Co=`GV9}S4wpg!wV$z9vcZ{}(5Wys;y+?0Xq5_;og zbS}@wo=^_sJ_ZEqO4JOW?gPgZ@iIb`&lXhMz4Olg^=g%|YL1r%Mn517oE2>JnKNPZ zZ4FHo8W)aE(2PA`b6=(5sTqnKmYLVl79CoyKh?*WGdr8kg1SKub6OV=Pxqg`Gi-2Q z4!5bTi5z?6d|}{a9ch1uq+#G}hf?l=aRYa3U;o_|Bks*?3Ic>d`&9I=ik(wZ_qI7P z#t2axIZmLS=~-ub>(qYJjX89O5*fEEAXut~!Bv;MJ9<9A2(bh?ykIizhkezxE%n2+ zWQ*DpjNKenX3;OKbxp-+XQGEuwLoNZL7&sX9OUs-c ztE-8%2teSzk2F(L*VGrH-yVGN`~7Oy8t^n28#{SudzmaIUOV2|UyF-P++}HY6R=$4 z=lf{p2)2gu&}L10M(FId{a(_w5tz}LM%(d{vMu#x?VUBhzZ8s4N*?g1qjx(t_Z+7| z{Qfjx3gOVy=x|ID#c-w$R;suVlycE1lYGfE`w({Bkr10{ene2gR-5{QqDCUFWExWQ zkMNd%+}AO-;h~@N_$&xg+y-s|rF45E!f!%Iw%UhpD0V7@W(;=Bd`q$8@MZm5S`?A1 z790k7{g%>4v4G#j<6)@yPPp}9&0?=@b8TH%ZX@rI8uVebQO zA$UiDjN1(mJleGIJ#j>PVp|hgYanc#S${8FPH*eeL0&z7TqWno&eZhxlrRbmscfK< z4&%X&9QU)M;nrbhf$X5=(4 zSJrYAQLca*yJD_Ih)rEFu-5S~3lGe43<6PZ8jf~Ff`SM_*-~00=GWqdg8~B3R*6~K zsv#mRLX^Q{pblT3%vQsTN-#+YM6xhFm8cgsn9BMc5UK8Lix#TPGL(#QvyD{fVBJ$2+T4PDH%H?=dWVm&1?h2P;-T zBEFvon2DJ1_F1o#(LC3S&4XEBF{;;_2TpD^9>el{lS?)2JLiEcu#`OcvJk#DjH-A= zL|gO`73g16VO#)I`dc2A5cE|=MA>pvs%5iT z$XkZWhYF|ip~ACq%mX9W(3t1>RbGBrWp0wUl?!Tv0?nd2zsW;0Zwr{z6wCmXTB{Ot z`HIgkyPMI}D)X|u{Gv>G$3h-9WR+P_&P<+c#`R0-J3qC01!JF9A;0XaqG42JP$z;a zEWYQh92+NDC_R)uq+z?Ti9ZG1Q*ayb`7Tf*z`l79wBilURV~aZVj(RUGxH%1?|`8-f!tI+ z)o1>iitl+;RWdb8^TZzq%yn-9XFlri;68Hd>%HYmP6aAUM-&5g?S!xV-_%&SNIYK-1O^Unk zi>4xmDo@Y9tf|_Wj2z|9S2-+>;0%XPo{zYD{N>_*F3EMwPmvY3;0Ws%kXXJ7S&hS` zE-tv(M$C)qmnzb?(%kKjKo;|0_G!h2HCwtj9}fNJx+EB6oP}QI94sz)}=W-exVp6bUD99Tws}sXk3xi1#h^Y2QMu0;=`J?IhLW zv8rR^Y9_A~Cue8;^0U52GwvJ08^eJL z7it?MV0)3Px3IEib&9l{yS(I|vcAbjkbH3ONdoj|v^O1)pIhqjQT}=lCFZp1sz6ePiA8j8D4l5|U?SU*vTKhM=k+uy|SyNnKp2A2){ zQP2vY3E9|6eG)|=%rbGx{F6VOFTv6?$T2H*UnKdF^BN-tJz9Z+)NS2Z>vh1&w7MOg z9&|^hT8V;|yy0fR(a5*3trd&Ywv@dR=*r47RrysnSUaOB&HNQ2{CZ1-au7?iuqmbe zs&g!MyQ!JHjIOTq2qnS67I_zY?gBl{UZqRWZaYSGSA*=iX2O*CU;1P5-GjDUP<`dD zCt(0>rE7qw45Qj}fVs`(xBH#G-Z=W98EpnFt<;?C*MX*Qb2E9M2rRZRx=QS#aYl@g z=Jb(EHN6>C2pd=RfQB7PwO@4EuEbd%j!5$&85}INSv=%T*z%^mZEJLAzflux=Sx_N zQdhPRhIvKB*R2!NCbpK9!WE;lCbvZnlU{~4Gt4-KK*$}5zsjw8&QuOm#kw;oI^PbKNI=CB+J@jcU$>%q) zKMxR=5L(k!F$-%qOhHqjF>z7>_mj1xaE90hh|+*Kbl1&%$!p#Yk_9&mVH$M5?C=mx)wd-fd7ueUYR*wftII-6qe@RMWMp@EdS z0U+v5lKeYGA_8e6=T8cxpv`#x2fX2Gpr}M_xBap8*B|3uYuGP>^;gX!*codyt0FsIa~ea?I?O9XfJF>@f%@VO*^0}cc>utuRvFWgekeXsQJZiIACO;y4GCL%Bzh9&Ltc!6|v&7NFtTfS=gBHGB)if@>Mm_G5tbhF2r5Xym|$ zeOKCtT&ZSPA%cS+cbsy4>(z$Eiaq`ifooTK#`p?*R3#aO;ApIGGCnHzworI7w$YC1 zMrp|?F}54UZAIq%Zd7t>?z;`2lMQdl1DBttYyN8%g9DDic@`L!) zYg)5T_)?tJuM9=k(^X^RXSEGu)NXv|t}$igCaYzovyaGCfTiO<6hk4~FrMJd;$}ed z$-LW)(S42>7`g?%c-KGJ;u99e4cO9$Wd#V$c{?TomjJ;u*HoW0Ik{fDC_o@2$%D*4 zfnwKW)l1V4tymS!t44}dRqnrZ;Gnr_DCLxg78enmMoGChA1QY*D#!V9FG3pGivo9m zp{cz@nD)o!@B5WrIP3|E#W>94P4kvwv1Q_>46FGQp7N{)A%A9dX)j6uZb8>U@kI%M z*x8HH0g=muS~{+0vfqjCd9Gn9=z1nf+zFjfket3g+A+1aXqm5>kMA9yqFc%La;-{% zyD_d0S)_u~S$)I|ZPRM677NdM!rj1>n9ZPObszFeMN=N^L$ew3JRmL5KuHZ<=YD1LLcfFb)!6gQ%yN zUG*P*9uE~&+^BW~gtySEO@|T#*IMziEiT+;G4{mmBYSuw+yDyN3#}^9MUc-COa={} zMc?UH%xvXlv+lC=*xG912TC|U zII{f*3qd@uvOT(ApHx#k?zpMXcRwI;Q{RolI!Eo=!Q=-FekS63v9-Wt)`j7-HN3?;ugDJ4_?~-I z>niWGkgrO0axM{SCLb{PvdvKxhk-B5vHC$n7HX@XUAJ91{hWA_L?*jFbpq`c-+tj{OvkOMf z{^#O3e?VE43Ryo57cM>QHjVcPW0T7*QjfUby!A+$9-hU}L7S2Clh~ z{lf$|Eq_^B;`SdtHxy~0$(6b)Ww{o-a~p^LJb8e@U}Kw~3$0q8y2H%m1BVIQX3TW4 zFfWZV*c3rEv5h>C)k*RBrN)5dEz@_CTP)gsXtJ0k$dH%_CmhP7fN-WB@%#6~$^LgJ z^#O$M*feCMY$(;Q!|Eit40(su$}3{7GT5W{n)-F0EJTj**)_`Dmo)iz7&NT-;jECW zbY-}3avDxwdBe5xyF&nH3j$_e4X11rrHYOm{?J+F)jRG8Qgb+GrNPJkng2GoemahK zi$H(jKDdak;je0LHC1Cx--j&57F#NI7|{rUjtM(ei%h(kD&xS9~=|f#I42>wo`fihS^fhm1@3Ji5Fm;Y)^oE0mex#Zt4jk zg(U#W)r03h2&b&f@Kfmz=2u?}t`(p1;}7)g1h_^YD0ULPeM)5fS-j4Tq(FWyiKO83 zc&0{D+DSZ>Z1uD9T6rvm--LoXjTI*N_X&ZwM*O`uSSbb_9vi=VoYzl?YhwVsxil79Az+WM?i3Qi23>LpQ~Tm z1Y^5F=ujR{S81x`gtD#TD9e-kEg+BF=}p&rnI}DwlA8} zxkZmh)1$NCG2eHoCjRszpN$*6xAeizr*5(|6@D81NMV0MX59e6n|#WbS{Q6GWI9in z6sh+8k&*zRjr@^5-osO=uc|GFaJkJD5V#s6n$*zZ&99f=RpQx^(s#0BDER^q)#v94 z6b^yqGObimp=+2DDO#Y$t&gQXIr1%7!O$`OXF5l%f9Owi_7>X0_Z-?n{ix8t`V$5H zg|>h9ldyxQE0_1Hcyt)=W>*H45);KB(tN|ju%E{s<*9)pgR&DT_99qrF_CgI@$5H| zEV<|8uaf9GJ957(!qKYuD!jsrHhD7byOW2`;8t|<0^0BV6v|y9F9rKE2(hswM6R62_~85*JG*>xH=H0pWe_O7Rlv^t^` z4!sm>Fr;eD=4CRBIchybQ`+#YEuu=UT7|`25h$%q>3^X~zQnW51y)r;t*7#Cm_JR7 zse!GVIK~E^T4LlN898ig`ZW05qS57yGsYZ6X3{YQqQ;}CQmTWlxDg^_8a;y)^Gj9x zej52+hIA|ZVkX~9xN2norX?nvTW7?~Cp+o?OG0WT^9v(?WB*_CP0$sRH<77!66wPX z;m)0ITS} zTE`BvXf|8u`~rmKPPxt&T#R?p#aY7A%smv_we377cJ-Az4VAJ~bDjx69*mZ4{XVgF zLc7n#BJ&j?WUY>;$~dS{F;rFtP~TwfzKc(b{B1n5WI9Kw{QtnJGH^;G=a^|6!0V&f zlHs4%MszXq`XBoBCGV;P{s)<2G|ZK$R&AyKoedMMCD>66x>RDWX!NB4$2ZK{)Q2aj zN~719OHMG!k{2L&g8SQq>ql z*-j`{zrc|~)_8RsP{)4fTgLI`8)Lev5c!5@tx`*^%Bd;1DUlR(8(lFJLl`eje;Dij zBzE6DG1fvJ_)`dTDe*Qc2Cr zlm%R^>i?-~{N?zpt+0TGW6YA=7KkP}7Q5lEMO#xQa3_t|j06eBczA^F15F^mNS&w(rc&}Y`65pVVzA%fyX1h8H+e=pmsU3jSY z5?5Xq05S->Th5edaG)h)8;~Ncd%A#zc4jVb*KCk?9hPsV;k4 zO1IY5T1+8$H<<1SraOSoV%p5OL5oFcy*k}9tU7k;fw8oK$l=`>MUx_je@>Xl$1Jcr zkEi{@>vPraT};mR!B4&lQS^PpYtoj8neyGNJ$7jhpU%nX9ER7Z^;WClG?wn&XANE^ zI$hC99t-c;CV!GKJa`2rdKtMtfI-3H$KVILv)VJu$m=13PzWsVd5B3yu|%N(wuzMe z5D}I7c<0DaoxgsKI5%`#Ppw(yM6!FNYw*8cSxC(v#<}T|-l@7$ zS|fy2lAv0mod3hb&5_59Ikl4L25==GKycR$-R-l^T{rBW5h5^&EVDqpFCduu%HLv! zWEZJ^-U#u7LVZK?&|VRZYbF?TW+YKC%gt=YofSri6a_adWaZ~eb9Xl} z=A2HVjlh*M0l}Qzk01SFMdR0djSw%AD3ht1uMpJdH_WO!WY&^tMu_srVcbLi%w1Qe zT#GlxoH{GW=?QQf1A=Q+q_l6HU1keT8zDL|6b%ebc%&ia=9ShQ3n>(@ry z0^3T)MO#Tv!C8TAB}0^7K|z2p)KOaV`0Ln8(W^SoH&QRKtz_H|D<~1T+7+8=*Hc|^ zv8P+iG!0O@vB~7{3^b=Ei$RU5Hm-YZc%$+#2^?N^$90;>VL3c$(xaP4J8Paz}#Opl%6R?5W|u>?4%}1P?K9*7XgTe6D)mBkr zejs1R?t$D7qEYe;9r{oBjnc%H-?K$k&7<^?vO}PnC`vS)_|3y^_ zZKLqQAhA@vy>J^n`-Jb}u0dQiqFj<#aT{g4Kvf;_XJ&b7Uv)zB+Ap&lRRwu~Smm2jCGAIqkQ_zrto(tf8x=tEDM78cNH6%R*>2uIZA!hnn$b2nYf5 zWKI4%5*ut^_z~=?S0l>T56Yy~3Mr%|6YOA&0m&Gyx}afG3C-lQL&UVIS|3=p{PMXh zr4azEkfXc*LYQ(za%Ck2>=Y{Gf8?Wg<5MH~Bo=##!0Nw~0$-u&&d2@7jg;C6J1K!7 z=YSTQg>*xfS>P!fu}46rwQ47&10wepNL6+vMN1iW1CtF}Nx%3<6~=e$rpVn5D~!nD zVfIP?yANMa8_(y!lof<)J1OjSZY$&ZMqGl!@A?!9DvEB?&CkP@#&i`7k`18*=C`=kg3vYm2#@quo?-BE-Fb~~hy z#aomkF8ahQ-%Zmah5YczU|K&+x7>gfn$5VTH8ujQu*Qz9SLboGqa8aa-1VTblTzpg zaHV;G;8t1pV(ib|7FXuwOO_xuUad_b%XgrD5D+}ow5@T!az@;es!Da>SzJX9x66e` zW3Jq>Yqv$o0rkfz6wJ8VRAFd-TJ?w3wZgM}j1c9K!?@;Fqjx1OZLS6)(1Yrv(njD) zrY`C9P;TUuVnKrUv1+{kja=Dc#&{!fwM)-PC8zgb;{_mjrv0eTX7fg)*YE*OMU6io zhXqvpcEd+)=Fe5SCEGQNT@(#msRJs>xYieEes^HasV54q2QJUqMah83dsnD>NvQhF zcXw!PDg}H1#ut~hs>-J*g*TMK^v$LJ+16dCX z{`0%A!18%4#r7JFnreMk<)F*u6PH@9VncJgr{8B8c3}uQooW=7CYMb27KOM<^o#n* zH@u2s2JERcmBcYVn= zy2UJSwQ1C`L%F-dCSd#RA~JVd0gU zssdK(gMF-SJY?A8^rtd}%7;i4E2;HKp)k%;u+`81hcl%n)#^)5aQRuTGRapT=c8EOmNZ z7$a_EHp!OSuIivrFg*7fr7b1XeyzueOOrGCQjPHVT0aCik`!Q zn;tpTW)Nk`p=zb`8T^ijkx!Ezbof(?JL<>>kJGfTVyfCMYPA(D%N@E6>ro019mo9- z{+T+*H_+HQufZK)zcx59v*W||wfGP%4|lNDN@tRa92FgV{F@i`4)tD>XP2Jnfi~s< zE`OA=;3H@|#*wZVT2dLKsY5aJJY`IgoI#1b+zV?~bP95#6LMLyI};g4D~ss^w9}5# zvtsz$tMdTKzH?5mIb9#0Z+;n&>^8z5Q0uNX+n4r_rWcoc`WJRi>4pxX8;SsO^|qCh z4mZA0MyW0aFQJ2Zudn{LwsL>{ACx+GKE*E>sWL=`1TYR+x9t0A4}*148s7y_P68XvW70>LSF%buXsg&-0@QE_xN>43?e(hfdD_LjgfVL;H45|gQeCA4A68A<}A zHt7uQv4m0yizxqHRQd0ps&k?3XT(Ior^Y%L%cgNF6z))UVS-$>htANd(t5{Ye*zNc zc4B?c(v{LEq4im+VP)Fz&>Aa!pi*pEvM8gMOqwVYt;x5cW6wP6~cksjYn5{QB=Sj!NNpFgF4&_4T#*6 zE6!~_ykFA%F~-pTh;x)|4T(YtXrcAjGKF1k+nwAr};r_RxLf56KxkJ7ex}$#67sI2xB2xvyU6 zdGiQ42TRQwjmnk!_hQ8#yf*%!YxNIczd29S%0bfV-?_b5PH$C2{cG6q$8~m8#8z)t zEUBU%6~MnrRM%GTN8z@5=LwDL3#m=^@;Gfawq#EJeU*Jfhdc4#ov*X?+VvWj%@$_0 z6Zdegyu8zL>jf#MP>?v^Gxz%LB~rpK)+O;?2DN#zp@`MUVgG!njCZcx{SGIO4Y^)) zOjzis(F2BSG(k0w47*Tr^MvgyBE-4QHLIgyFCTrH%>aV|;dT4Ov%X!*1?SHnFeXG{LpVeXn*ltW-r2T)aX8a!v=(f_N^b$XL$b+!y@~Hh4!!aLqr(D zDh4Mnhv;Yr`POkSn)_HUrMkA)m(bD`cYTHOxsWI+_jf%QFiLu)uS^4d^kI}UMPHn} zmg~z?z!beitET7&(3PqBA1f8WipnrEMwMdpHLd!D52`n06uyfe_1vAPiG#ihmFlY3 z)A9~_D|*reugMO2UFtDOzq}H~*2B*$K6+p4qWuR%4hS0&?&M1Cm+SF&+}r9cXk48a3{y NcTGJwu1MnO{|ATXB$faG delta 127010 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 - + + + + + diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx new file mode 100644 index 00000000..4d366985 --- /dev/null +++ b/src/AuthContext.tsx @@ -0,0 +1,143 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file AuthContext.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { createContext, useState, useContext, useEffect, useCallback } from 'react' +import { useWagmiAccount } from '@skalenetwork/metaport' +import { SiweMessage } from 'siwe' + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api' + +interface AuthContextType { + isSignedIn: boolean + handleSignIn: () => Promise + handleSignOut: () => Promise + checkSignInStatus: () => Promise + getSignInStatus: () => Promise +} + +const AuthContext = createContext(undefined) + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isSignedIn, setIsSignedIn] = useState(false) + const { address } = useWagmiAccount() + + const checkSignInStatus = useCallback(async () => { + if (!address) { + setIsSignedIn(false) + return + } + try { + const status = await getSignInStatus() + if (status) { + setIsSignedIn(true) + } else { + await handleSignOut() + } + } catch (error) { + console.error('Error checking sign-in status:', error) + setIsSignedIn(false) + } + }, [address]) + + const getSignInStatus = async (): Promise => { + try { + if (!address) return false + const response = await fetch(`${API_URL}/auth/status`, { + credentials: 'include' + }) + const data = await response.json() + return data.isSignedIn && data.address && data.address.toLowerCase() === address.toLowerCase() + } catch (error) { + console.error('Error checking sign-in status:', error) + return false + } + } + + const handleSignIn = async () => { + if (!address) return + try { + const message = new SiweMessage({ + domain: window.location.host, + address: address, + statement: 'Sign in with Ethereum to the SKALE Portal.', + uri: window.location.origin, + version: '1', + chainId: 1, + nonce: await fetchNonce() + }) + const signature = await window.ethereum.request({ + method: 'personal_sign', + params: [message.prepareMessage(), address] + }) + const response = await fetch(`${API_URL}/auth/signin`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message, signature }), + credentials: 'include' + }) + if (response.ok) { + setIsSignedIn(true) + } + } catch (error) { + console.error('Error signing in:', error) + } + } + + const handleSignOut = async () => { + try { + await fetch(`${API_URL}/auth/signout`, { + method: 'POST', + credentials: 'include' + }) + } catch (error) { + console.error('Error signing out:', error) + } finally { + setIsSignedIn(false) + } + } + + const fetchNonce = async () => { + const response = await fetch(`${API_URL}/auth/nonce`) + const data = await response.json() + return data.nonce + } + + useEffect(() => { + checkSignInStatus() + }, [address, checkSignInStatus]) + + return ( + + {children} + + ) +} + +export const useAuth = () => { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/src/LikedAppsContext.tsx b/src/LikedAppsContext.tsx new file mode 100644 index 00000000..c1f93de5 --- /dev/null +++ b/src/LikedAppsContext.tsx @@ -0,0 +1,121 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * @file LikedAppsContext.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { createContext, useState, useContext, useEffect, useCallback } from 'react' +import { useWagmiAccount } from '@skalenetwork/metaport' +import { useAuth } from './AuthContext' + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api' + +interface LikedAppsContextType { + likedApps: string[] + isLoading: boolean + error: string | null + toggleLikedApp: (appId: string) => Promise + refreshLikedApps: () => Promise + getAppId: (chainName: string, appName: string) => string +} + +const LikedAppsContext = createContext(undefined) + +export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [likedApps, setLikedApps] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const { address } = useWagmiAccount() + const { isSignedIn } = useAuth() + + const fetchLikedApps = useCallback(async () => { + if (!isSignedIn || !address) { + setLikedApps([]) + setIsLoading(false) + return + } + setIsLoading(true) + setError(null) + try { + const response = await fetch(`${API_URL}/apps/liked`, { + credentials: 'include' + }) + if (!response.ok) { + throw new Error('Failed to fetch liked apps') + } + const data = await response.json() + setLikedApps(data.liked_apps) + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred') + } finally { + setIsLoading(false) + } + }, [isSignedIn, address]) + + const toggleLikedApp = async (appId: string) => { + const isLiked = likedApps.includes(appId) + const endpoint = isLiked ? `${API_URL}/apps/unlike` : `${API_URL}/apps/like` + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ app_id: appId }), + credentials: 'include' + }) + + if (!response.ok) { + throw new Error('Failed to update liked status') + } + + setLikedApps((prev) => (isLiked ? prev.filter((id) => id !== appId) : [...prev, appId])) + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred') + } + } + + useEffect(() => { + if (isSignedIn && address) { + fetchLikedApps() + } else { + setLikedApps([]) + setIsLoading(false) + } + }, [isSignedIn, address, fetchLikedApps]) + + const getAppId = (chainName: string, appName: string) => `${chainName}--${appName}` + + return ( + + {children} + + ) +} + +export const useLikedApps = () => { + const context = useContext(LikedAppsContext) + if (context === undefined) { + throw new Error('useLikedApps must be used within a LikedAppsProvider') + } + return context +} diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index ed9c7565..328734fb 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -15,21 +15,43 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - /** * @file ConnectWallet.tsx * @copyright SKALE Labs 2023-Present */ -import Button from '@mui/material/Button' +import { useEffect } from 'react' +import { Button } from '@mui/material' import LooksRoundedIcon from '@mui/icons-material/LooksRounded' -import { cmn, cls, SkPaper, RainbowConnectButton } from '@skalenetwork/metaport' +import { cmn, cls, SkPaper, useWagmiAccount, RainbowConnectButton } from '@skalenetwork/metaport' +import { useAuth } from '../AuthContext' export default function ConnectWallet(props: { tile?: boolean className?: string customText?: string }) { + const { address } = useWagmiAccount() + const { isSignedIn, handleSignIn } = useAuth() + + useEffect(() => { + if (address && !isSignedIn) { + handleSignIn() + } + }, [address]) + + const handleButtonClick = (openConnectModal: any) => { + if (address) { + if (!isSignedIn) { + handleSignIn() + } + } else { + openConnectModal() + } + } + + if (isSignedIn) return null + return (
@@ -44,9 +66,7 @@ export default function ConnectWallet(props: { {({ openConnectModal }) => { return ( + +
+
+
+
) } ) diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 824a8ed4..68bb9f30 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -90,7 +90,12 @@ export default function AppCard(props: { - + ) } diff --git a/src/components/ecosystem/FavoriteApps.tsx b/src/components/ecosystem/FavoriteApps.tsx index 9f1c840b..72b025d9 100644 --- a/src/components/ecosystem/FavoriteApps.tsx +++ b/src/components/ecosystem/FavoriteApps.tsx @@ -21,10 +21,94 @@ * @copyright SKALE Labs 2024-Present */ -import React from 'react' +import { useEffect } from 'react' +import { types } from '@/core' +import { useLikedApps } from '../../LikedAppsContext' +import AppCard from './AppCardV2' +import { Button, Grid } from '@mui/material' +import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded' -const FavoriteApps: React.FC = () => { - return
Favorite Apps Component (To be implemented)
-} +import { cls, cmn, SkPaper } from '@skalenetwork/metaport' +import { useAuth } from '../../AuthContext' +import Carousel from '../Carousel' +import ConnectWallet from '../ConnectWallet' +import { Link } from 'react-router-dom' + +export default function FavoriteApps(props: { + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap + useCarousel?: boolean +}) { + const { likedApps, error, refreshLikedApps } = useLikedApps() + const { isSignedIn } = useAuth() + + useEffect(() => { + if (isSignedIn) { + refreshLikedApps() + } + }, [isSignedIn, refreshLikedApps]) + + if (!isSignedIn) return + if (error) return
Error: {error}
-export default FavoriteApps + const appCards = likedApps.map((appId) => { + const [chainName, appName] = appId.split('--') + return ( + + + + ) + }) + + if (appCards.length === 0) + return ( + +
+

+ You don't have any favorites yet +

+ {props.useCarousel && ( +
+
+
+ + + +
+
+
+ )} +
+
+ ) + + if (props.useCarousel) { + return {appCards} + } + + return ( + + {appCards} + + ) +} diff --git a/src/components/ecosystem/FavoriteIconButton.tsx b/src/components/ecosystem/FavoriteIconButton.tsx new file mode 100644 index 00000000..38c97422 --- /dev/null +++ b/src/components/ecosystem/FavoriteIconButton.tsx @@ -0,0 +1,89 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file FavoriteIconButton.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useEffect } from 'react' +import { IconButton, Tooltip } from '@mui/material' +import FavoriteIcon from '@mui/icons-material/Favorite' +import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder' +import { cls, cmn, useWagmiAccount, useConnectModal } from '@skalenetwork/metaport' +import { useLikedApps } from '../../LikedAppsContext' +import { useAuth } from '../../AuthContext' + +interface FavoriteIconButtonProps { + chainName: string + appName: string +} + +const FavoriteIconButton: React.FC = ({ chainName, appName }) => { + const { likedApps, toggleLikedApp, refreshLikedApps, getAppId } = useLikedApps() + const { isSignedIn, handleSignIn, getSignInStatus } = useAuth() + const { address } = useWagmiAccount() + const { openConnectModal } = useConnectModal() + const appId = getAppId(chainName, appName) + const isLiked = likedApps.includes(appId) + + const [asyncLike, setAsyncLike] = React.useState(false) + + const handleToggleLike = async () => { + setAsyncLike(true) + if (!address) { + openConnectModal?.() + return + } + await toggleLikedApp(appId) + refreshLikedApps() + } + + const handleAsyncLike = async () => { + if (!isSignedIn) { + await handleSignIn() + const status = await getSignInStatus() + if (!status) { + console.log('Sign-in failed or was cancelled') + return + } + } + await toggleLikedApp(appId) + } + + useEffect(() => { + if (asyncLike) { + setAsyncLike(false) + handleAsyncLike() + } + }, [address, isSignedIn]) + + return ( + + + {isLiked ? ( + + ) : ( + + )} + + + ) +} + +export default FavoriteIconButton diff --git a/src/components/ecosystem/Socials.tsx b/src/components/ecosystem/Socials.tsx index f4e66405..87087e48 100644 --- a/src/components/ecosystem/Socials.tsx +++ b/src/components/ecosystem/Socials.tsx @@ -23,12 +23,7 @@ import React from 'react' import { IconButton, Tooltip } from '@mui/material' -import { - LanguageRounded, - FavoriteBorderOutlined, - WavesRounded, - TrackChangesRounded -} from '@mui/icons-material' +import { LanguageRounded, WavesRounded, TrackChangesRounded } from '@mui/icons-material' import { SocialIcon } from 'react-social-icons/component' import 'react-social-icons/discord' import 'react-social-icons/github' @@ -36,19 +31,20 @@ import 'react-social-icons/telegram' import 'react-social-icons/x' import { cmn, cls } from '@skalenetwork/metaport' import { type types } from '@/core' +import FavoriteIconButton from './FavoriteIconButton' interface SocialButtonsProps { social?: types.AppSocials - onAddToFavorites?: () => void - isFavorite?: boolean + chainName?: string + appName?: string className?: string size?: 'sm' | 'md' } const SocialButtons: React.FC = ({ social, - onAddToFavorites, - isFavorite = false, + chainName, + appName, className, size = 'sm' }) => { @@ -100,7 +96,7 @@ const SocialButtons: React.FC = ({ if (!link) return null return ( -
+
= ({
)} {!social &&
} - {!isMd && ( - - - - - - )} + {!isMd && chainName && appName ? ( + + ) : null}
) } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 09b7630a..87840c7e 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -38,6 +38,7 @@ import WidgetsRoundedIcon from '@mui/icons-material/WidgetsRounded' import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' import HubRoundedIcon from '@mui/icons-material/HubRounded' import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' +import FavoriteBorderOutlinedIcon from '@mui/icons-material/FavoriteBorderOutlined' import ChainLogo from '../components/ChainLogo' import SkStack from '../components/SkStack' @@ -56,6 +57,8 @@ import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explore import { MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../core/constants' import SocialButtons from '../components/ecosystem/Socials' import AppCategoriesChips from '../components/ecosystem/CategoriesShips' +import { useLikedApps } from '../LikedAppsContext' +import { useAuth } from '../AuthContext' export default function App(props: { mpc: MetaportCore @@ -65,6 +68,9 @@ export default function App(props: { chainsMeta: types.ChainsMetadataMap }) { let { chain, app } = useParams() + const { likedApps, toggleLikedApp, getAppId } = useLikedApps() + const { isSignedIn, handleSignIn } = useAuth() + if (chain === undefined || app === undefined) return 'No such app' const network = props.mpc.config.skaleNetwork @@ -78,9 +84,18 @@ export default function App(props: { const appAlias = getChainAlias(props.chainsMeta, chain, app) const appMeta = chainMeta.apps?.[app]! const appDescription = appMeta.description ?? 'No description' - // const dAppRadarUrl = `${DAPP_RADAR_BASE_URL}${appMeta.dappradar ?? app}` - const expolorerUrl = getExplorerUrl(network, chain) + const appId = getAppId(chain, app) + const isLiked = likedApps.includes(appId) + + const handleFavoriteClick = async () => { + if (!isSignedIn) { + await handleSignIn() + } + await toggleLikedApp(appId) + } + + const explorerUrl = getExplorerUrl(network, chain) const isAppChain = chainMeta.apps && Object.keys(chainMeta.apps).length === 1 @@ -165,9 +180,10 @@ export default function App(props: {

{appAlias}

@@ -239,7 +255,7 @@ export default function App(props: { className={cls(styles.fullHeight)} title={`Contract ${index + 1}`} value={contractAddress} - url={addressUrl(expolorerUrl, contractAddress)} + url={addressUrl(explorerUrl, contractAddress)} /> ))} diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index bff45068..e50a8aa2 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -27,17 +27,16 @@ import { Helmet } from 'react-helmet' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' import Box from '@mui/material/Box' -import { Button, Tab, Tabs } from '@mui/material' +import { Tab, Tabs } from '@mui/material' import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded' -import EditRoundedIcon from '@mui/icons-material/EditRounded' import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded' import StarRoundedIcon from '@mui/icons-material/StarRounded' import { type types } from '@/core' -import { cmn, cls, type MetaportCore, styles } from '@skalenetwork/metaport' +import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' import { META_TAGS } from '../core/meta' import CategoryDisplay from '../components/ecosystem/Categories' import { @@ -58,7 +57,7 @@ import AllApps from '../components/ecosystem/AllApps' import NewApps from '../components/ecosystem/NewApps' import FavoriteApps from '../components/ecosystem/FavoriteApps' import TrendingApps from '../components/ecosystem/TrendingApps' -import { MAX_APPS_DEFAULT, SUBMIT_PROJECT_URL } from '../core/constants' +import { MAX_APPS_DEFAULT } from '../core/constants' export default function Ecosystem(props: { mpc: MetaportCore @@ -194,25 +193,13 @@ export default function Ecosystem(props: { chainsMeta={props.chainsMeta} /> )} - {activeTab === 2 && } + {activeTab === 2 && ( + + )} {activeTab === 3 && } - - diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index 2ab924fa..17936f9a 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -35,6 +35,7 @@ import { Button } from '@mui/material' import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' +import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' import LabelImportantRoundedIcon from '@mui/icons-material/LabelImportantRounded' import RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded' import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' @@ -52,6 +53,7 @@ import { MAX_APPS_DEFAULT } from '../core/constants' import NewApps from '../components/ecosystem/NewApps' import Carousel from '../components/Carousel' import Headline from '../components/Headline' +import FavoriteApps from '../components/ecosystem/FavoriteApps' export default function Start(props: { isXs: boolean @@ -131,7 +133,17 @@ export default function Start(props: { - +
+ } /> + + + +
+
Date: Thu, 15 Aug 2024 12:53:14 +0100 Subject: [PATCH 24/39] Add likes api, add trending apps tab, update categories, improve mobile version --- src/App.scss | 2 +- src/LikedAppsContext.tsx | 44 +++++++++++++--- src/Router.tsx | 4 -- src/SkBottomNavigation.tsx | 10 +++- src/SkDrawer.tsx | 32 ++++++------ src/components/AccountMenu.tsx | 4 +- src/components/Breadcrumbs.tsx | 2 +- src/components/ChainCard.tsx | 4 +- src/components/ConnectWallet.tsx | 8 ++- src/components/HelpZen.tsx | 2 +- src/components/MoreMenu.tsx | 10 ++-- src/components/delegation/Delegation.tsx | 4 +- src/components/ecosystem/AppCard.tsx | 2 +- src/components/ecosystem/Categories.tsx | 11 ++-- src/components/ecosystem/CategoryIcons.tsx | 17 ++++--- src/components/ecosystem/TrendingApps.tsx | 59 ++++++++++++++++++++-- src/core/constants.ts | 3 ++ src/core/ecosystem/categories.ts | 6 ++- src/pages/App.tsx | 10 +++- src/pages/Ecosystem.tsx | 15 ++++-- src/pages/Start.tsx | 24 +-------- 21 files changed, 185 insertions(+), 88 deletions(-) diff --git a/src/App.scss b/src/App.scss index 12d5e531..5223611f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -68,7 +68,7 @@ body { font-family: inherit !important; } -.fullWidth { +.fullW { width: 100%; } diff --git a/src/LikedAppsContext.tsx b/src/LikedAppsContext.tsx index c1f93de5..cf389348 100644 --- a/src/LikedAppsContext.tsx +++ b/src/LikedAppsContext.tsx @@ -9,25 +9,28 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . */ /** * @file LikedAppsContext.tsx * @copyright SKALE Labs 2024-Present */ - import React, { createContext, useState, useContext, useEffect, useCallback } from 'react' import { useWagmiAccount } from '@skalenetwork/metaport' import { useAuth } from './AuthContext' +import { API_URL, LIKES_REFRESH_INTERVAL } from './core/constants' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api' +interface AppLikes { + [appId: string]: number +} interface LikedAppsContextType { likedApps: string[] + appLikes: AppLikes isLoading: boolean error: string | null toggleLikedApp: (appId: string) => Promise @@ -39,6 +42,7 @@ const LikedAppsContext = createContext(undefin export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [likedApps, setLikedApps] = useState([]) + const [appLikes, setAppLikes] = useState({}) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const { address } = useWagmiAccount() @@ -68,10 +72,22 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi } }, [isSignedIn, address]) + const fetchAppLikes = useCallback(async () => { + try { + const response = await fetch(`${API_URL}/apps/likes`) + if (!response.ok) { + throw new Error('Failed to fetch app likes') + } + const data = await response.json() + setAppLikes(data) + } catch (err) { + console.error('Error fetching app likes:', err) + } + }, []) + const toggleLikedApp = async (appId: string) => { const isLiked = likedApps.includes(appId) const endpoint = isLiked ? `${API_URL}/apps/unlike` : `${API_URL}/apps/like` - try { const response = await fetch(endpoint, { method: 'POST', @@ -81,17 +97,23 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi body: JSON.stringify({ app_id: appId }), credentials: 'include' }) - if (!response.ok) { throw new Error('Failed to update liked status') } setLikedApps((prev) => (isLiked ? prev.filter((id) => id !== appId) : [...prev, appId])) + await fetchAppLikes() } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred') } } + useEffect(() => { + fetchAppLikes() + const interval = setInterval(fetchAppLikes, LIKES_REFRESH_INTERVAL) + return () => clearInterval(interval) + }, [fetchAppLikes]) + useEffect(() => { if (isSignedIn && address) { fetchLikedApps() @@ -105,7 +127,15 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi return ( {children} diff --git a/src/Router.tsx b/src/Router.tsx index 737d2208..e3a3be9a 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -54,7 +54,6 @@ import { getValidators } from './core/delegation/validators' import { initContracts } from './core/contracts' import { getStakingInfoMap } from './core/delegation/staking' import { formatSChains } from './core/chain' -import { getTopAppsByTransactions } from './core/explorer' import { loadMeta } from './core/metadata' @@ -68,7 +67,6 @@ export default function Router() { const [chainsMeta, setChainsMeta] = useState(null) const [schains, setSchains] = useState([]) const [metrics, setMetrics] = useState(null) - const [topApps, setTopApps] = useState(null) const [stats, setStats] = useState(null) const [termsAccepted, setTermsAccepted] = useState(false) const [stakingTermsAccepted, setStakingTermsAccepted] = useState(false) @@ -146,7 +144,6 @@ export default function Router() { const response = await fetch(`https://${endpoint}/files/metrics.json`) const metricsJson = await response.json() setMetrics(metricsJson) - setTopApps(getTopAppsByTransactions(metricsJson.metrics, 10)) } catch (e) { console.log('Failed to load metrics') console.error(e) @@ -240,7 +237,6 @@ export default function Router() { diff --git a/src/SkBottomNavigation.tsx b/src/SkBottomNavigation.tsx index a0066716..aeb035c5 100644 --- a/src/SkBottomNavigation.tsx +++ b/src/SkBottomNavigation.tsx @@ -31,6 +31,7 @@ import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontal import PieChartOutlineOutlinedIcon from '@mui/icons-material/PieChartOutlineOutlined' import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' export default function SkBottomNavigation() { const [value, setValue] = useState(0) @@ -69,8 +70,15 @@ export default function SkBottomNavigation() { }} /> } + onClick={() => { + navigate('/ecosystem') + }} + /> + } onClick={() => { navigate('/chains') }} diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index def2aa18..965c1719 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -47,7 +47,7 @@ export default function SkDrawer() { - + @@ -60,7 +60,7 @@ export default function SkDrawer() {

Bridge

- + - + - + - + - + - + - + @@ -104,7 +104,7 @@ export default function SkDrawer() {

Network

- + - + - + - + - + -
+ diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx index 4d50e572..d37f4ccb 100644 --- a/src/components/AccountMenu.tsx +++ b/src/components/AccountMenu.tsx @@ -141,13 +141,13 @@ export default function AccountMenu(props: any) { ) }} - + Transfers history (
{section.url ? ( - +
-
+
- {appCards} + Date: Thu, 15 Aug 2024 13:38:51 +0100 Subject: [PATCH 25/39] Update chain categories, update chain cards, cleanup code --- packages/core/src/types/ChainsMetadata.ts | 2 +- packages/core/src/types/index.ts | 3 +- src/components/ChainCategories.tsx | 44 ------------- src/components/SchainDetails.tsx | 19 +----- src/components/chains/ChainActions.tsx | 12 ---- src/components/chains/ChainCard.tsx | 9 +-- src/components/chains/ChainsSection.tsx | 11 +++- src/components/chains/HubApps.tsx | 66 +++++-------------- .../chains/tabs/ChainTabsSection.tsx | 2 - src/components/ecosystem/AppCardV2.tsx | 2 +- src/components/ecosystem/CategoriesShips.tsx | 66 +++++++++++-------- src/components/ecosystem/CategoryIcons.tsx | 8 +-- src/pages/App.tsx | 2 +- src/pages/Chains.tsx | 2 +- 14 files changed, 80 insertions(+), 168 deletions(-) delete mode 100644 src/components/ChainCategories.tsx diff --git a/packages/core/src/types/ChainsMetadata.ts b/packages/core/src/types/ChainsMetadata.ts index 4747c411..def7e598 100644 --- a/packages/core/src/types/ChainsMetadata.ts +++ b/packages/core/src/types/ChainsMetadata.ts @@ -28,7 +28,7 @@ export interface ChainMetadata { shortAlias?: string minSfuelWei?: string faucetUrl?: string - category: string | string[] + categories: CategoriesMap background?: string gradientBackground?: string description?: string diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index aeef4034..91bb33aa 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -27,7 +27,8 @@ export { AppMetadataMap, AppSocials, NetworksMetadataMap, - AppWithTimestamp + AppWithTimestamp, + CategoriesMap } from './ChainsMetadata' export * as staking from './staking' diff --git a/src/components/ChainCategories.tsx b/src/components/ChainCategories.tsx deleted file mode 100644 index e90683ec..00000000 --- a/src/components/ChainCategories.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file CategoryBadge.tsx - * @copyright SKALE Labs 2023-Present - */ - -import { cmn, cls } from '@skalenetwork/metaport' -import CategoryBadge, { isString } from './CategoryBadge' - -export default function ChainCategories(props: { - category: string | string[] | undefined - alias: string - isXs: boolean -}) { - if (!props.category) return - return ( -
- {isString(props.category) ? ( - - ) : ( - props.category.map((cat: string) => ( - - )) - )} -
- ) -} diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index 61dc8ea2..bb00f0c3 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -50,7 +50,6 @@ import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' import SkStack from './SkStack' import ChainLogo from './ChainLogo' -import ChainCategories from './ChainCategories' import Tile from './Tile' import Breadcrumbs from './Breadcrumbs' import CollapsibleDescription from './CollapsibleDescription' @@ -63,7 +62,7 @@ import { getRpcUrl, getChainId, HTTPS_PREFIX, getChainDescription } from '../cor import { getExplorerUrl } from '../core/explorer' import { formatNumber } from '../core/timeHelper' import ChainTabsSection from './chains/tabs/ChainTabsSection' -import Ship from './Ship' +import CategoriesChips from './ecosystem/CategoriesShips' export default function SchainDetails(props: { schainName: string @@ -190,22 +189,8 @@ export default function SchainDetails(props: {
- +
- - } - />

{chainAlias}

diff --git a/src/components/chains/ChainActions.tsx b/src/components/chains/ChainActions.tsx index 6e25c816..e7f1e4fa 100644 --- a/src/components/chains/ChainActions.tsx +++ b/src/components/chains/ChainActions.tsx @@ -27,14 +27,12 @@ import { cls, cmn } from '@skalenetwork/metaport' import { type types } from '@/core' import LanguageIcon from '@mui/icons-material/Language' import ViewInArRoundedIcon from '@mui/icons-material/ViewInArRounded' -import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined' import { getExplorerUrl } from '../../core/explorer' interface ChainActionsProps { chainMeta: types.ChainMetadata schainName: string skaleNetwork: types.SkaleNetwork - onConnectChain: () => void className?: string } @@ -42,7 +40,6 @@ const ChainActions: React.FC = ({ chainMeta, schainName, skaleNetwork, - onConnectChain, className }) => { const explorerUrl = getExplorerUrl(skaleNetwork, schainName) @@ -74,15 +71,6 @@ const ChainActions: React.FC = ({ - - onConnectChain()} - className={cls([cmn.pPrim, isMd], ['bgBlack', isMd])} - > - - -
) } diff --git a/src/components/chains/ChainCard.tsx b/src/components/chains/ChainCard.tsx index 35f84bcd..5ec97040 100644 --- a/src/components/chains/ChainCard.tsx +++ b/src/components/chains/ChainCard.tsx @@ -37,6 +37,7 @@ import ChainActions from './ChainActions' import Ship from '../Ship' import { formatNumber } from '../../core/timeHelper' +import CategoriesChips from '../ecosystem/CategoriesShips' const ChainCard: React.FC<{ skaleNetwork: types.SkaleNetwork @@ -46,14 +47,8 @@ const ChainCard: React.FC<{ }> = ({ skaleNetwork, schain, chainsMeta, transactions }) => { const shortAlias = getChainShortAlias(chainsMeta, schain.name) const url = `/chains/${shortAlias}` - const chainMeta = chainsMeta[schain.name] - const handleConnectChain = () => { - // Implement the logic to connect to the chain - console.log(`Connecting to chain: ${schain.name}`) - } - return ( @@ -87,12 +82,12 @@ const ChainCard: React.FC<{
+ ) diff --git a/src/components/chains/ChainsSection.tsx b/src/components/chains/ChainsSection.tsx index 88c6378e..ad41ead5 100644 --- a/src/components/chains/ChainsSection.tsx +++ b/src/components/chains/ChainsSection.tsx @@ -26,6 +26,8 @@ import { Grid } from '@mui/material' import { cls, cmn } from '@skalenetwork/metaport' import { type types } from '@/core' +import { getChainAlias } from '../../core/metadata' + import ChainCard from './ChainCard' import Headline from '../Headline' @@ -50,11 +52,18 @@ const ChainsSection: React.FC = ({ }) => { const gridSize = size === 'lg' ? { xs: 12, md: 6 } : { xs: 12, md: 4 } + // Sort schains based on chain alias + const sortedSchains = [...schains].sort((a, b) => { + const aliasA = getChainAlias(chainsMeta, a.name).toLowerCase() + const aliasB = getChainAlias(chainsMeta, b.name).toLowerCase() + return aliasA.localeCompare(aliasB) + }) + return (
- {schains.map((schain) => ( + {sortedSchains.map((schain) => ( (false) - const chainMeta = props.chainsMeta[props.schainName] const appCards: ReactElement[] = [] - if (chainMeta.apps) { - for (const appName in sortObjectByKeys(chainMeta.apps)) { - if (chainMeta.apps.hasOwnProperty(appName)) { - appCards.push( - - - - ) - } + if (!chainMeta.apps) return + + for (const appName in sortObjectByKeys(chainMeta.apps)) { + if (chainMeta.apps.hasOwnProperty(appName)) { + appCards.push( + + + + ) } - } else { - return } return ( - {show || props.all ? appCards : appCards.length === 4 ? appCards : appCards.slice(0, 3)} - {!props.all && appCards.length > 4 ? ( - - -
{ - setShow(!show) - }} - className={cls('br__tile', 'pointer')} - style={{ background: props.bg ? chainBg(props.chainsMeta, props.schainName) : '' }} - > -
-
-
-

- {show ? 'Hide apps' : `+${appCards.length - 3} Apps`} -

-
-
-
-
-
-
- ) : null} + {appCards}
) } diff --git a/src/components/chains/tabs/ChainTabsSection.tsx b/src/components/chains/tabs/ChainTabsSection.tsx index 91ffcc1f..1487af09 100644 --- a/src/components/chains/tabs/ChainTabsSection.tsx +++ b/src/components/chains/tabs/ChainTabsSection.tsx @@ -111,8 +111,6 @@ export default function ChainTabsSection(props: { skaleNetwork={network} chainsMeta={props.chainsMeta} schainName={props.schainName} - bg={true} - all={true} />
diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 68bb9f30..3ed07d98 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -88,7 +88,7 @@ export default function AppCard(props: { )}
- + = ({ app, className }) => { +const CategoriesChips: React.FC = ({ categories, className }) => { const [expanded, setExpanded] = useState(false) const chips = useMemo(() => { - if (!app.categories) return [] + if (!categories) return [] - const getCategoryName = (tag: string) => categories[tag]?.name ?? tag + const getCategoryName = (tag: string) => categoriesData[tag]?.name ?? tag const getSubcategoryName = (categoryTag: string, subcategoryTag: string): string => { - const category = categories[categoryTag] + const category = categoriesData[categoryTag] if (!category) return subcategoryTag if (Array.isArray(category.subcategories)) { return category.subcategories.includes(subcategoryTag) ? subcategoryTag : subcategoryTag } - return category.subcategories[subcategoryTag]?.name ?? subcategoryTag + return category.subcategories && typeof category.subcategories === 'object' + ? category.subcategories[subcategoryTag]?.name ?? subcategoryTag + : subcategoryTag } - return Object.entries(app.categories).flatMap(([categoryTag, subcategories]) => [ - } - />, - ...(Array.isArray(subcategories) - ? subcategories.map((subTag) => ( - } - /> - )) - : []) - ]) - }, [app.categories]) + if (Array.isArray(categories)) { + return categories.map((categoryTag) => ( + } + /> + )) + } else { + return Object.entries(categories).flatMap(([categoryTag, subcategories]) => [ + } + />, + ...(Array.isArray(subcategories) + ? subcategories.map((subTag) => ( + } + /> + )) + : []) + ]) + } + }, [categories]) if (chips.length === 0) return null @@ -85,4 +97,4 @@ const AppCategoriesChips: React.FC = ({ app, className ) } -export default AppCategoriesChips +export default CategoriesChips diff --git a/src/components/ecosystem/CategoryIcons.tsx b/src/components/ecosystem/CategoryIcons.tsx index d0c1e05e..6a3405dd 100644 --- a/src/components/ecosystem/CategoryIcons.tsx +++ b/src/components/ecosystem/CategoryIcons.tsx @@ -55,20 +55,20 @@ import { PsychologyOutlined, SportsBaseballOutlined, PrecisionManufacturingOutlined, - AutoAwesomeRounded, + AutoAwesomeOutlined, HikingRounded, FlagRounded, FlareRounded, CandlestickChartRounded, JoinRightRounded, PhoneIphoneOutlined, - DiamondRounded + DiamondOutlined } from '@mui/icons-material' export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { switch (category) { case 'ai': - return + return case 'dao': return case 'data-information': @@ -76,7 +76,7 @@ export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { case 'defi': return case 'digital-collectibles': - return + return case 'entertainment': return case 'explorer': diff --git a/src/pages/App.tsx b/src/pages/App.tsx index ab9ea272..d73ad62f 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -176,7 +176,7 @@ export default function App(props: {
- +
- -
-
-
-
- ) - } -) + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) -AllApps.displayName = 'AllApps' + return ( + + {apps.map((app: types.AppWithChainAndName) => { + const appId = getAppId(app.chain, app.appName) + const isTrending = trendingAppIds.includes(appId) + const isNew = isNewApp({ chain: app.chain, app: app.appName }, newApps) + return ( + + + + ) + })} + + ) +} export default AllApps diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 3ed07d98..5aa628fb 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -33,8 +33,7 @@ import { chainBg, getChainAlias } from '../../core/metadata' import CollapsibleDescription from '../CollapsibleDescription' import AppCategoriesChips from './CategoriesShips' import SocialButtons from './Socials' -import { Chip } from '@mui/material' -import { isNewApp } from '../../core/ecosystem/utils' +import { ShipTrending, ShipNew, ShipPreTge } from '../Ship' export default function AppCard(props: { skaleNetwork: types.SkaleNetwork @@ -43,12 +42,12 @@ export default function AppCard(props: { chainsMeta: types.ChainsMetadataMap transactions?: number newApps?: types.AppWithTimestamp[] + isTrending?: boolean + isNew?: boolean }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) const url = `/ecosystem/${shortAlias}/${props.appName}` const appMeta = props.chainsMeta[props.schainName]?.apps?.[props.appName] - const isNew = - props.newApps && isNewApp({ chain: props.schainName, app: props.appName }, props.newApps) if (!appMeta) return @@ -82,10 +81,9 @@ export default function AppCard(props: {

{getChainAlias(props.chainsMeta, props.schainName, props.appName)}

- {isNew && } - {appMeta.tags?.includes('pretge') && ( - - )} + {props.isTrending && } + {props.isNew && } + {appMeta.tags?.includes('pretge') && }
diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx index eced5ec7..d5769a73 100644 --- a/src/components/ecosystem/TrendingApps.tsx +++ b/src/components/ecosystem/TrendingApps.tsx @@ -21,14 +21,13 @@ * @copyright SKALE Labs 2024-Present */ -import React, { useMemo } from 'react' +import React from 'react' import { type types } from '@/core' import { useLikedApps } from '../../LikedAppsContext' import AppCard from './AppCardV2' import { Box, Grid } from '@mui/material' import { cls } from '@skalenetwork/metaport' import Carousel from '../Carousel' -import { MAX_APPS_DEFAULT } from '../../core/constants' interface TrendingAppsProps { skaleNetwork: types.SkaleNetwork @@ -37,45 +36,42 @@ interface TrendingAppsProps { } const TrendingApps: React.FC = ({ skaleNetwork, chainsMeta, useCarousel }) => { - const { appLikes } = useLikedApps() + const { getTrendingApps, getAppInfoById } = useLikedApps() + const trendingAppIds = getTrendingApps() - const trendingApps = useMemo(() => { - return Object.entries(appLikes) - .sort(([, likesA], [, likesB]) => likesB - likesA) - .slice(0, MAX_APPS_DEFAULT) - .map(([appId, likes]) => { - const [chainName, appName] = appId.split('--') - return { chainName, appName, likes } - }) - }, [appLikes]) - - const appCards = trendingApps.map(({ chainName, appName }) => ( - - - - )) + const appCards = trendingAppIds.map((appId) => { + const { chain, app } = getAppInfoById(appId) + return ( + + + + ) + }) if (useCarousel) return {appCards} return ( - {trendingApps.map(({ chainName, appName }) => ( - - - - - - ))} + {trendingAppIds.map((appId) => { + const { chain, app } = getAppInfoById(appId) + return ( + + + + + + ) + })} ) } diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts index f42d3b09..23e6efe9 100644 --- a/src/core/ecosystem/apps.ts +++ b/src/core/ecosystem/apps.ts @@ -23,13 +23,8 @@ import { type types } from '@/core' import { getChainAlias } from '../metadata' -export interface AppWithChainAndName extends types.AppMetadata { - chain: string - appName: string -} - -export function getAllApps(chainsMetadata: types.ChainsMetadataMap): AppWithChainAndName[] { - const allApps: AppWithChainAndName[] = [] +export function getAllApps(chainsMetadata: types.ChainsMetadataMap): types.AppWithChainAndName[] { + const allApps: types.AppWithChainAndName[] = [] for (const [chainName, chainData] of Object.entries(chainsMetadata)) { if (chainData.apps) { @@ -46,14 +41,14 @@ export function getAllApps(chainsMetadata: types.ChainsMetadataMap): AppWithChai return allApps } -export function sortAppsByAlias(apps: AppWithChainAndName[]): AppWithChainAndName[] { +export function sortAppsByAlias(apps: types.AppWithChainAndName[]): types.AppWithChainAndName[] { return apps.sort((a, b) => a.alias.localeCompare(b.alias)) } export function filterAppsByCategory( - apps: AppWithChainAndName[], + apps: types.AppWithChainAndName[], checkedItems: string[] -): AppWithChainAndName[] { +): types.AppWithChainAndName[] { if (checkedItems.length === 0) return apps return apps.filter((app) => { if (!app.categories || Object.keys(app.categories).length === 0) return false @@ -73,10 +68,10 @@ export function filterAppsByCategory( } export function filterAppsBySearchTerm( - apps: AppWithChainAndName[], + apps: types.AppWithChainAndName[], searchTerm: string, chainsMeta: types.ChainsMetadataMap -): AppWithChainAndName[] { +): types.AppWithChainAndName[] { if (!searchTerm || searchTerm === '') return apps const st = searchTerm.toLowerCase() return apps.filter( diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 427c69b9..9f130abe 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -40,7 +40,6 @@ import { cmn, cls, type MetaportCore } from '@skalenetwork/metaport' import { META_TAGS } from '../core/meta' import CategoryDisplay from '../components/ecosystem/Categories' import { - AppWithChainAndName, filterAppsByCategory, filterAppsBySearchTerm, getAllApps, @@ -68,7 +67,7 @@ export default function Ecosystem(props: { useUrlParams() const allApps = useMemo(() => sortAppsByAlias(getAllApps(props.chainsMeta)), [props.chainsMeta]) const [checkedItems, setCheckedItems] = useState([]) - const [filteredApps, setFilteredApps] = useState([]) + const [filteredApps, setFilteredApps] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState(0) From e796fe3a850440a655c4243255e57ef7affdde97 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 15 Aug 2024 15:31:13 +0100 Subject: [PATCH 27/39] Add new and trending apps to appcard component --- src/components/ecosystem/FavoriteApps.tsx | 19 ++++++++++----- src/components/ecosystem/NewApps.tsx | 28 +++++++++++------------ src/components/ecosystem/TrendingApps.tsx | 11 ++++++++- src/pages/Ecosystem.tsx | 2 ++ src/pages/Start.tsx | 8 ++++++- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/components/ecosystem/FavoriteApps.tsx b/src/components/ecosystem/FavoriteApps.tsx index 72b025d9..c6a1a98c 100644 --- a/src/components/ecosystem/FavoriteApps.tsx +++ b/src/components/ecosystem/FavoriteApps.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { types } from '@/core' import { useLikedApps } from '../../LikedAppsContext' import AppCard from './AppCardV2' @@ -33,14 +33,17 @@ import { useAuth } from '../../AuthContext' import Carousel from '../Carousel' import ConnectWallet from '../ConnectWallet' import { Link } from 'react-router-dom' +import { isNewApp } from '../../core/ecosystem/utils' export default function FavoriteApps(props: { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap useCarousel?: boolean + newApps: types.AppWithTimestamp[] }) { - const { likedApps, error, refreshLikedApps } = useLikedApps() + const { likedApps, error, refreshLikedApps, getAppInfoById, getTrendingApps } = useLikedApps() const { isSignedIn } = useAuth() + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) useEffect(() => { if (isSignedIn) { @@ -52,10 +55,12 @@ export default function FavoriteApps(props: { if (error) return
Error: {error}
const appCards = likedApps.map((appId) => { - const [chainName, appName] = appId.split('--') + const { chain, app } = getAppInfoById(appId) + const isTrending = trendingAppIds.includes(appId) + const isNew = isNewApp({ chain, app }, props.newApps) return ( ) diff --git a/src/components/ecosystem/NewApps.tsx b/src/components/ecosystem/NewApps.tsx index 549c4228..e8b53f42 100644 --- a/src/components/ecosystem/NewApps.tsx +++ b/src/components/ecosystem/NewApps.tsx @@ -21,12 +21,13 @@ * @copyright SKALE Labs 2024-Present */ -import React from 'react' +import React, { useMemo } from 'react' import { Grid, Box } from '@mui/material' import { cls } from '@skalenetwork/metaport' import AppCard from './AppCardV2' import Carousel from '../Carousel' import { type types } from '@/core' +import { useLikedApps } from '../../LikedAppsContext' interface NewAppsProps { newApps: { chain: string; app: string; added: number }[] @@ -41,33 +42,32 @@ const NewApps: React.FC = ({ chainsMeta, useCarousel = false }) => { - const appCards = newApps.map((app) => ( - + const { getTrendingApps, getAppId } = useLikedApps() + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + const renderAppCard = (app: { chain: string; app: string }) => { + const appId = getAppId(app.chain, app.app) + const isTrending = trendingAppIds.includes(appId) + return ( - - )) + ) + } if (useCarousel) { - return {appCards} + return {newApps.map(renderAppCard)} } return ( {newApps.map((app) => ( - - - + {renderAppCard(app)} ))} diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx index d5769a73..4defcd36 100644 --- a/src/components/ecosystem/TrendingApps.tsx +++ b/src/components/ecosystem/TrendingApps.tsx @@ -28,14 +28,21 @@ import AppCard from './AppCardV2' import { Box, Grid } from '@mui/material' import { cls } from '@skalenetwork/metaport' import Carousel from '../Carousel' +import { isNewApp } from '../../core/ecosystem/utils' interface TrendingAppsProps { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap useCarousel?: boolean + newApps: types.AppWithTimestamp[] } -const TrendingApps: React.FC = ({ skaleNetwork, chainsMeta, useCarousel }) => { +const TrendingApps: React.FC = ({ + skaleNetwork, + chainsMeta, + useCarousel, + newApps +}) => { const { getTrendingApps, getAppInfoById } = useLikedApps() const trendingAppIds = getTrendingApps() @@ -59,6 +66,7 @@ const TrendingApps: React.FC = ({ skaleNetwork, chainsMeta, u {trendingAppIds.map((appId) => { const { chain, app } = getAppInfoById(appId) + const isNew = isNewApp({ chain, app }, newApps) return ( @@ -67,6 +75,7 @@ const TrendingApps: React.FC = ({ skaleNetwork, chainsMeta, u schainName={chain} appName={app} chainsMeta={chainsMeta} + isNew={isNew} /> diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 9f130abe..021aba20 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -200,12 +200,14 @@ export default function Ecosystem(props: { )} {activeTab === 3 && ( )} diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx index caf67fb1..9b957d23 100644 --- a/src/pages/Start.tsx +++ b/src/pages/Start.tsx @@ -123,6 +123,7 @@ export default function Start(props: { skaleNetwork={props.skaleNetwork} chainsMeta={props.chainsMeta} useCarousel={true} + newApps={newApps} />
See all
- + Date: Thu, 15 Aug 2024 16:17:36 +0100 Subject: [PATCH 28/39] Add SKALE Social links, update ToS page --- src/App.scss | 5 ----- src/SkDrawer.tsx | 6 ++---- src/components/PageCard.tsx | 9 +++++++-- src/components/TermsModal.tsx | 12 ++++++------ src/core/constants.ts | 9 +++++++++ src/pages/Ecosystem.tsx | 18 ++++++++++++------ src/pages/Start.tsx | 4 +++- 7 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/App.scss b/src/App.scss index 689b7082..4f773957 100644 --- a/src/App.scss +++ b/src/App.scss @@ -943,11 +943,6 @@ input[type=number] { // md styling -a, -.a { - color: #71ffb8 !important; -} - .markdown { hr { border-color: $border-color; diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index 965c1719..fefe47d5 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -108,15 +108,13 @@ export default function SkDrawer() { - + diff --git a/src/components/PageCard.tsx b/src/components/PageCard.tsx index 79d11328..9bb7757a 100644 --- a/src/components/PageCard.tsx +++ b/src/components/PageCard.tsx @@ -25,9 +25,14 @@ import { Link } from 'react-router-dom' import { cmn, cls, SkPaper, styles } from '@skalenetwork/metaport' import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded' -export default function PageCard(props: { name: string; icon: any; description: string }) { +export default function PageCard(props: { + name: string + icon: any + description: string + url?: string +}) { return ( - +
diff --git a/src/components/TermsModal.tsx b/src/components/TermsModal.tsx index e5e49c2c..6e90da04 100644 --- a/src/components/TermsModal.tsx +++ b/src/components/TermsModal.tsx @@ -71,9 +71,9 @@ export default function TermsModal(props: { - +
- +

SKALE will NEVER ask you for your seed phrase or private keys

@@ -81,9 +81,9 @@ export default function TermsModal(props: { - +
- +

Make sure you are connected to the correct URL and only use this official link: - +

- +

Before you use the SKALE {title}, you must review the terms of service carefully and confirm below. diff --git a/src/core/constants.ts b/src/core/constants.ts index 9a5ff29a..00d37305 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -92,3 +92,12 @@ export const SUBMIT_PROJECT_URL = export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api' export const LIKES_REFRESH_INTERVAL = 20000 + +export const SKALE_SOCIAL_LINKS = { + x: 'https://twitter.com/skalenetwork', + telegram: 'https://t.me/skaleofficial', + discord: 'https://discord.com/invite/gM5XBy6', + github: 'https://github.com/skalenetwork', + swell: 'https://swell.skale.space/', + website: 'https://skale.space/' +} diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 021aba20..7d124e49 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -56,7 +56,8 @@ import AllApps from '../components/ecosystem/AllApps' import NewApps from '../components/ecosystem/NewApps' import FavoriteApps from '../components/ecosystem/FavoriteApps' import TrendingApps from '../components/ecosystem/TrendingApps' -import { MAX_APPS_DEFAULT } from '../core/constants' +import { MAX_APPS_DEFAULT, SKALE_SOCIAL_LINKS } from '../core/constants' +import SocialButtons from '../components/ecosystem/Socials' export default function Ecosystem(props: { mpc: MetaportCore @@ -119,12 +120,17 @@ export default function Ecosystem(props: { -

-

Ecosystem

+
+
+

Ecosystem

+

+ Explore dApps across the SKALE ecosystem +

+
+
+ +
-

- Explore dApps across the SKALE ecosystem -

} /> } /> From e1eeb143de4ca1134cc52cad6dda5c840eaad2c0 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 16 Aug 2024 12:28:38 +0100 Subject: [PATCH 29/39] Update skale-network submodule --- skale-network | 2 +- src/App.scss | 11 +++++++++-- src/components/ecosystem/FavoriteIconButton.tsx | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/skale-network b/skale-network index 7ed9e917..9f89c718 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit 7ed9e917028636643a78d944a9f1b94b1cca39eb +Subproject commit 9f89c71880ad03b2b79362edbca72898355c9a60 diff --git a/src/App.scss b/src/App.scss index 4f773957..93ebc3a7 100644 --- a/src/App.scss +++ b/src/App.scss @@ -875,8 +875,6 @@ input[type=number] { .ship_PORTAL { background: linear-gradient(180deg, #221d3c, #151225); color: #9681fc; - // background: linear-gradient(180deg, #103324, #091d14); - // color: #81fcd1; } .ship_SELF { @@ -917,7 +915,16 @@ input[type=number] { } } +.iconRed { + color: #da3a34 !important; + svg { + color: #da3a34 !important; + } +} +.btnRed { + background-color: #da3a34 !important; +} .shipNodes { margin-right: 5px; diff --git a/src/components/ecosystem/FavoriteIconButton.tsx b/src/components/ecosystem/FavoriteIconButton.tsx index 38c97422..ba3fd67a 100644 --- a/src/components/ecosystem/FavoriteIconButton.tsx +++ b/src/components/ecosystem/FavoriteIconButton.tsx @@ -77,7 +77,7 @@ const FavoriteIconButton: React.FC = ({ chainName, appN {isLiked ? ( - + ) : ( )} From 7b78aa41f0194e756c8b3441cfb1e8595d6c0c3b Mon Sep 17 00:00:00 2001 From: Dmytro Date: Fri, 16 Aug 2024 18:50:45 +0100 Subject: [PATCH 30/39] Improve error handling on app page, update testnet config --- config/legacy.ts | 6 +- config/staging.ts | 405 ------------------ config/testnet.ts | 6 +- skale-network | 2 +- src/LikedAppsContext.tsx | 2 +- .../ecosystem/FavoriteIconButton.tsx | 2 +- src/pages/App.tsx | 18 +- 7 files changed, 27 insertions(+), 414 deletions(-) delete mode 100644 config/staging.ts diff --git a/config/legacy.ts b/config/legacy.ts index 0d61d767..75d07b42 100644 --- a/config/legacy.ts +++ b/config/legacy.ts @@ -112,6 +112,8 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { }, theme: { mode: 'dark', - vibrant: true - } + vibrant: true, + primary: '#93B8EC', + background: '#000000', + }, } diff --git a/config/staging.ts b/config/staging.ts deleted file mode 100644 index ccfb2e8c..00000000 --- a/config/staging.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { type interfaces } from '@skalenetwork/metaport' - -export const METAPORT_CONFIG: interfaces.MetaportConfig = { - skaleNetwork: 'staging', - openOnLoad: true, - openButton: true, - debug: false, - chains: [ - 'mainnet', - 'staging-legal-crazy-castor', // Europa - 'staging-utter-unripe-menkar', // Calypso - 'staging-faint-slimy-achird', // Nebula - 'staging-fast-active-bellatrix', // Chaos Testnet - ], - tokens: { - eth: { - symbol: 'ETH' - }, - skl: { - decimals: '18', - name: 'SKALE', - symbol: 'SKL' - }, - usdc: { - decimals: '6', - symbol: 'USDC', - name: 'USD Coin' - }, - usdt: { - decimals: '6', - symbol: 'USDT', - name: 'Tether USD' - }, - wbtc: { - decimals: '8', - symbol: 'WBTC', - name: 'WBTC' - }, - _SPACE_1: { - name: 'SKALE Space', - symbol: 'SPACE', - iconUrl: - 'https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Rocket/3D/rocket_3d.png' - }, - _SKALIENS_1: { - name: 'SKALIENS Collection', - symbol: 'SKALIENS', - iconUrl: - 'https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Alien/3D/alien_3d.png' - }, - ruby: { - name: 'Ruby Token', - iconUrl: 'https://ruby.exchange/images/tokens/ruby-square.png', - symbol: 'RUBY' - }, - dai: { - name: 'DAI Stablecoin', - symbol: 'DAI' - }, - usdp: { - name: 'Pax Dollar', - symbol: 'USDP', - iconUrl: 'https://ruby.exchange/images/tokens/usdp-square.png' - }, - hmt: { - name: 'Human Token', - symbol: 'HMT', - iconUrl: 'https://s2.coinmarketcap.com/static/img/coins/64x64/10347.png' - }, - ubxs: { - name: 'UBXS Token', - symbol: 'UBXS', - decimals: '6', - iconUrl: 'https://s2.coinmarketcap.com/static/img/coins/64x64/17242.png' - } - }, - connections: { - mainnet: { - eth: { - eth: { - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-utter-unripe-menkar': { - hub: 'staging-legal-crazy-castor' - } - } - } - }, - erc20: { - skl: { - address: '0x493D4442013717189C9963a2e275Ad33bfAFcE11', - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-utter-unripe-menkar': { - hub: 'staging-legal-crazy-castor' - }, - 'staging-faint-slimy-achird': { - hub: 'staging-legal-crazy-castor' - } - } - }, - ruby: { - address: '0xd66641E25E9D36A995682572eaD74E24C11Bb422', - chains: { - 'staging-legal-crazy-castor': {} - } - }, - dai: { - address: '0x83B38f79cFFB47CF74f7eC8a5F8D7DD69349fBf7', - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-fast-active-bellatrix': { - hub: 'staging-legal-crazy-castor' - } - } - }, - usdp: { - address: '0x66259E472f8d09083ecB51D42F9F872A61001426', - chains: { - 'staging-legal-crazy-castor': {} - } - }, - usdt: { - address: '0xD1E44e3afd6d3F155e7704c67705E3bAC2e491b6', - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-fast-active-bellatrix': { - hub: 'staging-legal-crazy-castor' - } - } - }, - usdc: { - address: '0x85dedAA65D33210E15911Da5E9dc29F5C93a50A9', - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-utter-unripe-menkar': { - hub: 'staging-legal-crazy-castor' - }, - 'staging-faint-slimy-achird': { - hub: 'staging-legal-crazy-castor' - } - } - }, - wbtc: { - address: '0xd80BC0126A38c9F7b915e1B2B9f78280639cadb3', - chains: { - 'staging-legal-crazy-castor': {} - } - }, - hmt: { - address: '0x4058d058ff62ED347dB8a69c43Ae9C67268B50b0', - chains: {} - }, - ubxs: { - address: '0x5A4957cc54B21e1fa72BA549392f213030d34804', - chains: { - 'staging-legal-crazy-castor': {}, - 'staging-fast-active-bellatrix': { - hub: 'staging-legal-crazy-castor' - } - } - } - }, - erc721meta: { - _SPACE_1: { - address: '0x1b7729d7E1025A031aF9D6E68598b57f4C2adfF6', - chains: {} - } - }, - erc1155: { - _SKALIENS_1: { - address: '0x6cb73D413970ae9379560aA45c769b417Fbf33D6', - chains: {} - } - } - }, - 'staging-utter-unripe-menkar': { - // Calypso connections - eth: { - eth: { - address: '0xECabAE592Eb56D96115FcF4c7F772ADB7BF573d0', - chains: { - 'staging-legal-crazy-castor': { - clone: true - }, - mainnet: { - clone: true, - hub: 'staging-legal-crazy-castor' - } - } - } - }, - erc20: { - skl: { - address: '0x7E1B8750C21AebC3bb2a0bDf40be104C609a9852', - chains: { - 'staging-legal-crazy-castor': { - clone: true - }, - 'staging-faint-slimy-achird': { - hub: 'staging-legal-crazy-castor', - clone: true - }, - mainnet: { - hub: 'staging-legal-crazy-castor', - clone: true - } - } - }, - usdc: { - address: '0x49c37d0Bb6238933eEe2157e9Df417fd62723fF6', - chains: { - 'staging-legal-crazy-castor': { - clone: true - }, - mainnet: { - hub: 'staging-legal-crazy-castor', - clone: true - } - } - } - } - }, - 'staging-fast-active-bellatrix': { - // Chaos connections - erc20: { - ubxs: { - address: '0xB430a748Af4Ed4E07BA53454a8247f4FA0da7484', - chains: { - mainnet: { - clone: true, - hub: 'staging-legal-crazy-castor' - }, - 'staging-legal-crazy-castor': { - clone: true - } - } - }, - usdt: { - address: '0x082081c8e607ca6c1c53ac093cab3847ed59c0b0', - chains: { - mainnet: { - clone: true, - hub: 'staging-legal-crazy-castor' - }, - 'staging-legal-crazy-castor': { - clone: true - } - } - }, - dai: { - address: '0x08f98Af60eb83C18184231591A8F89577E46A4B9', - chains: { - mainnet: { - clone: true, - hub: 'staging-legal-crazy-castor' - }, - 'staging-legal-crazy-castor': { - clone: true - } - } - } - } - }, - 'staging-faint-slimy-achird': { // nebula connections - erc20: { - skl: { - address: '0x7F73B66d4e6e67bCdeaF277b9962addcDabBFC4d', - chains: { - 'staging-legal-crazy-castor': { - clone: true - }, - mainnet: { - hub: 'staging-legal-crazy-castor', - clone: true - }, - 'staging-utter-unripe-menkar': { - hub: 'staging-legal-crazy-castor', - clone: true - } - } - }, - usdc: { - address: '0x717d43399ab3a8aada669CDC9560a6BAfdeA9796', - chains: { - 'staging-legal-crazy-castor': { - clone: true - }, - mainnet: { - hub: 'staging-legal-crazy-castor', - clone: true - } - } - } - } - }, - 'staging-legal-crazy-castor': { - // Europa connections - eth: { - eth: { - address: '0xD2Aaa00700000000000000000000000000000000', - chains: { - mainnet: { - clone: true - }, - 'staging-utter-unripe-menkar': { - wrapper: '0xa270484784f043e159f74C03B691F80B6F6e3c24' - } - } - } - }, - erc20: { - skl: { - address: '0xbA1E9BA7CDd4815Da6a51586bE56e8643d1bEAb6', - chains: { - mainnet: { - clone: true - }, - 'staging-utter-unripe-menkar': { - wrapper: '0x6a679eF80aF3fE01A646F858Ca1e26D58b5430B6' - }, - 'staging-faint-slimy-achird': { - wrapper: '0x6a679eF80aF3fE01A646F858Ca1e26D58b5430B6' - } - } - }, - ruby: { - address: '0xf06De9214B1Db39fFE9db2AebFA74E52f1e46e39', - chains: { - mainnet: { - clone: true - } - } - }, - dai: { - address: '0x3595E2f313780cb2f23e197B8e297066fd410d30', - chains: { - mainnet: { - clone: true - }, - 'staging-fast-active-bellatrix': { - wrapper: '0x6075f63de307DC2280b7b1b98948885200B03093' - } - } - }, - usdp: { - address: '0xe0E2cb3A5d6f94a5bc2D00FAa3e64460A9D241E1', - chains: { - mainnet: { - clone: true - } - } - }, - usdt: { - address: '0xa388F9783d8E5B0502548061c3b06bf4300Fc0E1', - chains: { - mainnet: { - clone: true - }, - 'staging-fast-active-bellatrix': { - wrapper: '0xf8179aD86A964f2E856d11Dd7f4a280dCd721Aa3' - } - } - }, - usdc: { - address: '0x5d42495D417fcd9ECf42F3EA8a55FcEf44eD9B33', - chains: { - mainnet: { - clone: true - }, - 'staging-utter-unripe-menkar': { - wrapper: '0x4f250cCE5b8B39caA96D1144b9A32E1c6a9f97b0' - }, - 'staging-faint-slimy-achird': { - wrapper: '0x4f250cCE5b8B39caA96D1144b9A32E1c6a9f97b0' - } - } - }, - wbtc: { - address: '0xf5E880E1066DDc90471B9BAE6f183D5344fd289F', - chains: { - mainnet: { - clone: true - } - } - }, - ubxs: { - address: '0xaB5149362daCcC086bC4ABDde80aB6b09cBc118E', - chains: { - mainnet: { - clone: true - }, - 'staging-fast-active-bellatrix': { - wrapper: '0x8e55e1Cc37ecA9636F4eF35874468876d52d623F' - } - } - } - } - } - }, - theme: { - mode: 'dark', - vibrant: true - } -} diff --git a/config/testnet.ts b/config/testnet.ts index 7951de62..b7ebe616 100644 --- a/config/testnet.ts +++ b/config/testnet.ts @@ -301,6 +301,8 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { }, theme: { mode: 'dark', - vibrant: true - } + vibrant: true, + primary: '#93B8EC', + background: '#000000', + }, } diff --git a/skale-network b/skale-network index 9f89c718..af54d8ba 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit 9f89c71880ad03b2b79362edbca72898355c9a60 +Subproject commit af54d8badd25f465957b29c9f12f200d6b3744e7 diff --git a/src/LikedAppsContext.tsx b/src/LikedAppsContext.tsx index e08d49ae..1d6fb72f 100644 --- a/src/LikedAppsContext.tsx +++ b/src/LikedAppsContext.tsx @@ -77,7 +77,7 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi const fetchAppLikes = useCallback(async () => { try { - const response = await fetch(`${API_URL}/apps/likes`) + const response = await fetch(`${API_URL}/apps/all`) if (!response.ok) { throw new Error('Failed to fetch app likes') } diff --git a/src/components/ecosystem/FavoriteIconButton.tsx b/src/components/ecosystem/FavoriteIconButton.tsx index ba3fd67a..f98479d4 100644 --- a/src/components/ecosystem/FavoriteIconButton.tsx +++ b/src/components/ecosystem/FavoriteIconButton.tsx @@ -77,7 +77,7 @@ const FavoriteIconButton: React.FC = ({ chainName, appN {isLiked ? ( - + ) : ( )} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index d73ad62f..7d570603 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -59,6 +59,7 @@ import SocialButtons from '../components/ecosystem/Socials' import AppCategoriesChips from '../components/ecosystem/CategoriesShips' import { useLikedApps } from '../LikedAppsContext' import { useAuth } from '../AuthContext' +import ErrorTile from '../components/ErrorTile' export default function App(props: { mpc: MetaportCore @@ -79,10 +80,23 @@ export default function App(props: { chain = findChainName(props.chainsMeta, chain ?? '') const chainMeta = props.chainsMeta[chain] - if (!chainMeta) return 'No such chain' + if (!chainMeta) + return ( + + + + ) const appAlias = getChainAlias(props.chainsMeta, chain, app) - const appMeta = chainMeta.apps?.[app]! + const appMeta = chainMeta.apps?.[app] + + if (!appMeta) + return ( + + + + ) + const appDescription = appMeta.description ?? 'No description' const appId = getAppId(chain, app) From 2b4262419d19f93f40db1da15938ea4e5d8395c1 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 13:09:52 +0100 Subject: [PATCH 31/39] Update submodule --- skale-network | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skale-network b/skale-network index af54d8ba..c6cd48e7 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit af54d8badd25f465957b29c9f12f200d6b3744e7 +Subproject commit c6cd48e77db12cd6775a01eb41f63561af999d50 From dca26f99914eacbdf9213816982a3ced8b964241 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 13:16:53 +0100 Subject: [PATCH 32/39] Update SkDrawer --- src/SkBottomNavigation.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SkBottomNavigation.tsx b/src/SkBottomNavigation.tsx index aeb035c5..ea2bf2cf 100644 --- a/src/SkBottomNavigation.tsx +++ b/src/SkBottomNavigation.tsx @@ -42,8 +42,9 @@ export default function SkBottomNavigation() { setValue(500) if (location.pathname === '/') setValue(0) if (location.pathname === '/bridge' || location.pathname.includes('/transfer')) setValue(1) - if (location.pathname.includes('/chains') || location.pathname.includes('/admin')) setValue(2) - if (location.pathname.includes('/staking')) setValue(3) + if (location.pathname.includes('/ecosystem') || location.pathname.includes('/apps')) setValue(2) + if (location.pathname.includes('/chains') || location.pathname.includes('/admin')) setValue(3) + if (location.pathname.includes('/staking')) setValue(4) }, [location]) return ( From 2345ae1069e8b180fe800f140840a73dfd1ded5b Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 16:56:38 +0100 Subject: [PATCH 33/39] Minor UI fixes, updated submodule --- .gitmodules | 2 +- skale-network | 2 +- src/App.scss | 2 +- src/components/Paymaster.tsx | 6 ++- src/components/delegation/ValidatorCard.tsx | 56 +++++++++++---------- src/components/delegation/ValidatorInfo.tsx | 3 -- src/core/constants.ts | 2 +- src/data/changelog.mdx | 4 -- src/pages/App.tsx | 27 ++++++++-- src/pages/StakeAmount.tsx | 6 ++- 10 files changed, 67 insertions(+), 43 deletions(-) diff --git a/.gitmodules b/.gitmodules index f6438c69..4cb3b8d1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "skale-network"] path = skale-network url = https://github.com/skalenetwork/skale-network.git - branch = add-new-categories + branch = master [submodule "helper-scripts"] path = helper-scripts url = https://github.com/skalenetwork/helper-scripts.git diff --git a/skale-network b/skale-network index c6cd48e7..f9362124 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit c6cd48e77db12cd6775a01eb41f63561af999d50 +Subproject commit f93621243a822d34e5a44c2cc90c197d42f4894e diff --git a/src/App.scss b/src/App.scss index 93ebc3a7..546b881e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -595,7 +595,7 @@ code { .monthInputWrap { border-radius: 25px; - background: rgba(41, 255, 148, 0.08); + background: rgba(147, 184, 236, 0.16); padding-left: 20px; } diff --git a/src/components/Paymaster.tsx b/src/components/Paymaster.tsx index 644a8b24..3537f270 100644 --- a/src/components/Paymaster.tsx +++ b/src/components/Paymaster.tsx @@ -182,7 +182,11 @@ export default function Paymaster(props: { mpc: MetaportCore; name: string }) { return (
- } /> + } + className={cls(cmn.mtop20, cmn.mbott10)} + /> {!address ? ( ) : ( diff --git a/src/components/delegation/ValidatorCard.tsx b/src/components/delegation/ValidatorCard.tsx index 13a2e15a..28eb5ca1 100644 --- a/src/components/delegation/ValidatorCard.tsx +++ b/src/components/delegation/ValidatorCard.tsx @@ -25,7 +25,7 @@ import { Link } from 'react-router-dom' import Grid from '@mui/material/Grid' import Tooltip from '@mui/material/Tooltip' -import { cmn, cls, styles, fromWei } from '@skalenetwork/metaport' +import { cmn, cls, styles, fromWei, SkPaper } from '@skalenetwork/metaport' import ValidatorLogo from './ValidatorLogo' import { TrustBadge, ValidatorBadge } from './ValidatorBadges' @@ -49,17 +49,20 @@ export default function ValidatorCard(props: { return ( - -
+ { - props.validator.acceptNewRequests ? props.setValidatorId(props.validator.id) : null - }} >
@@ -119,26 +122,27 @@ export default function ValidatorCard(props: {
) : null}
- - {size !== 'lg' ? ( - -
-

- Min: {minDelegation} SKL -

-
-
- ) : null} - {size === 'lg' ? ( - -
-

- Address: {props.validator.validatorAddress} -

-
-
- ) : null} -
+
+ {size !== 'lg' && ( + +
+

+ Min: {minDelegation} SKL +

+
+
+ )} + {size === 'lg' && ( + +
+

+ Address: {props.validator.validatorAddress} +

+
+
+ )} +
+ ) diff --git a/src/components/delegation/ValidatorInfo.tsx b/src/components/delegation/ValidatorInfo.tsx index 0f5ffb14..9548a561 100644 --- a/src/components/delegation/ValidatorInfo.tsx +++ b/src/components/delegation/ValidatorInfo.tsx @@ -58,7 +58,6 @@ export default function ValidatorInfo(props: { validator: IValidator; className? value={`${Number(props.validator.feeRate) / 10}% fee`} text="Validator fee" grow - className="border" size="md" icon={} /> @@ -66,7 +65,6 @@ export default function ValidatorInfo(props: { validator: IValidator; className? value={props.validator.id.toString()} text="Validator ID" grow - className="border" size="md" icon={} /> @@ -74,7 +72,6 @@ export default function ValidatorInfo(props: { validator: IValidator; className? value={`${minDelegation} SKL`} text="Minimum delegation amount" grow - className="border" size="md" icon={} /> diff --git a/src/core/constants.ts b/src/core/constants.ts index 00d37305..f927efa0 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -81,7 +81,7 @@ export const STATS_API: { [key in types.SkaleNetwork]: string | null } = { } export const BASE_METADATA_URL = - 'https://raw.githubusercontent.com/skalenetwork/skale-network/add-new-categories/metadata/' // todo: tmp + 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' export const MAX_APPS_DEFAULT = 12 diff --git a/src/data/changelog.mdx b/src/data/changelog.mdx index 87328bee..a8d4d8c1 100644 --- a/src/data/changelog.mdx +++ b/src/data/changelog.mdx @@ -74,12 +74,8 @@ This release adds a brand new Home page with an instant access to main Portal pa #### 🔮 Chain management functionality -todo todo todo - #### 📱 Better mobile optimization, several UI improvements -todo todo todo - ### Bugfixes #### ⚒️ Minor internal bugfixes in the `metaport` library diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 7d570603..b4e5b2f8 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Helmet } from 'react-helmet' import { useParams } from 'react-router-dom' @@ -54,12 +54,14 @@ import { findChainName } from '../core/chain' import { formatNumber } from '../core/timeHelper' import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' -import { MAINNET_CHAIN_LOGOS, OFFCHAIN_APP } from '../core/constants' +import { MAINNET_CHAIN_LOGOS, MAX_APPS_DEFAULT, OFFCHAIN_APP } from '../core/constants' import SocialButtons from '../components/ecosystem/Socials' import AppCategoriesChips from '../components/ecosystem/CategoriesShips' import { useLikedApps } from '../LikedAppsContext' import { useAuth } from '../AuthContext' import ErrorTile from '../components/ErrorTile' +import { ShipNew, ShipPreTge, ShipTrending } from '../components/Ship' +import { getRecentApps, isNewApp } from '../core/ecosystem/utils' export default function App(props: { mpc: MetaportCore @@ -69,9 +71,14 @@ export default function App(props: { chainsMeta: types.ChainsMetadataMap }) { let { chain, app } = useParams() - const { likedApps, appLikes, toggleLikedApp, getAppId } = useLikedApps() + const { likedApps, appLikes, toggleLikedApp, getAppId, getTrendingApps } = useLikedApps() const { isSignedIn, handleSignIn } = useAuth() + const newApps = useMemo( + () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), + [props.chainsMeta] + ) + if (chain === undefined || app === undefined) return 'No such app' const network = props.mpc.config.skaleNetwork @@ -103,6 +110,9 @@ export default function App(props: { const isLiked = likedApps.includes(appId) const likesCount = appLikes[appId] || 0 + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + const isNew = isNewApp({ chain, app }, newApps) + const handleFavoriteClick = async () => { if (!isSignedIn) { await handleSignIn() @@ -201,7 +211,16 @@ export default function App(props: { {isLiked ? 'Favorite' : 'Add to favorites'}
-

{appAlias}

+ +
+

{appAlias}

+
+ {trendingAppIds.includes(appId) && } + {isNew && } + {appMeta.tags?.includes('pretge') && } +
+
+
diff --git a/src/pages/StakeAmount.tsx b/src/pages/StakeAmount.tsx index 21999122..b7ba8bbb 100644 --- a/src/pages/StakeAmount.tsx +++ b/src/pages/StakeAmount.tsx @@ -144,7 +144,11 @@ export default function StakeAmount(props: { )} - } /> + } + className={cls(cmn.mtop20, cmn.mbott10)} + /> {props.address ? ( Date: Mon, 19 Aug 2024 17:03:05 +0100 Subject: [PATCH 34/39] Update constants.ts --- src/core/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index 14cebfca..478c5b0f 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -81,4 +81,4 @@ export const STATS_API: { [key in interfaces.SkaleNetwork]: string | null } = { } export const BASE_METADATA_URL = - 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' + 'https://raw.githubusercontent.com/skalenetwork/skale-network/old-main/metadata/' From 7a9e20a0110591a8625f43622ed6ee97546ea11a Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 20:34:22 +0100 Subject: [PATCH 35/39] Fix AppChains bugs, add section with other chains, rename Chip component, change icons --- src/App.scss | 60 +++++++-------- src/SkDrawer.tsx | 2 +- src/components/CategoryBadge.tsx | 4 +- src/components/CategorySection.tsx | 76 ------------------- src/components/{Ship.tsx => Chip.tsx} | 20 ++--- src/components/SchainDetails.tsx | 2 +- src/components/chains/ChainActions.tsx | 4 +- src/components/chains/ChainCard.tsx | 24 +++--- src/components/chains/ChainsSection.tsx | 1 - src/components/chains/HubTile.tsx | 2 +- src/components/chains/tabs/DeveloperInfo.tsx | 5 -- src/components/delegation/Delegation.tsx | 4 +- src/components/delegation/ValidatorCard.tsx | 10 +-- src/components/ecosystem/AppCardV2.tsx | 10 +-- ...ategoriesShips.tsx => CategoriesChips.tsx} | 12 +-- src/components/ecosystem/CategoryIcons.tsx | 10 +-- src/pages/App.tsx | 10 +-- src/pages/Chains.tsx | 53 +++++++++---- src/styles/{ship.scss => chip.scss} | 0 vercel.json | 2 +- 20 files changed, 126 insertions(+), 185 deletions(-) delete mode 100644 src/components/CategorySection.tsx rename src/components/{Ship.tsx => Chip.tsx} (70%) rename src/components/ecosystem/{CategoriesShips.tsx => CategoriesChips.tsx} (96%) rename src/styles/{ship.scss => chip.scss} (100%) diff --git a/src/App.scss b/src/App.scss index 546b881e..398f879c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,6 +1,6 @@ @import './variables'; @import './styles/components'; -@import './styles/ship'; +@import './styles/chip'; :root { background: black; @@ -729,7 +729,7 @@ input[type=number] { /* Firefox */ } -.shipNew { +.ChipNew { margin-right: 5px; background: #93B8EC; border-radius: 20px; @@ -741,7 +741,7 @@ input[type=number] { } } -.shipTrending { +.chipTrending { background: linear-gradient(180deg, #e56d36, #D0602D) !important; p { @@ -749,7 +749,7 @@ input[type=number] { } } -.shipNewApp { +.chipNewApp { background: linear-gradient(180deg, #65a974, #508d5e) !important; p { @@ -757,14 +757,14 @@ input[type=number] { } } -.shipPreTge { +.chipPreTge { background: linear-gradient(180deg, #EBB84F, #bc923b) !important; p { color: black !important } } -.shipXs { +.chipXs { border-radius: 20px; padding: 3px 6px; @@ -774,7 +774,7 @@ input[type=number] { } } -.shipSm { +.chipSm { border-radius: 20px; padding: 6px 12px; @@ -784,7 +784,7 @@ input[type=number] { } } -.shipXs { +.chipXs { border-radius: 15px; padding: 4px 8px; @@ -794,12 +794,12 @@ input[type=number] { } } -.skShip { +.skChip { background: linear-gradient(180deg, rgb(52 52 52), rgb(31 31 31)); } -.ship { +.chip { border-radius: 20px; padding: 6px 12px; width: max-content; @@ -810,89 +810,89 @@ input[type=number] { } } -.ship_pretge { +.chip_pretge { background: linear-gradient(180deg, #E7B443, #b1882f); color: #201808; } -.ship_new { +.chip_new { color: #09100b; background: linear-gradient(#5C9B6A, #477a52); } -.ship_DELEGATED { +.chip_DELEGATED { background: linear-gradient(180deg, #0f3d29, #0a2a1c); color: #3cda94; } -.ship_ACCEPTED { +.chip_ACCEPTED { background: linear-gradient(180deg, #233d0f, #0a1b07); color: #3cda4e; } -.ship_REJECTED { +.chip_REJECTED { background: linear-gradient(180deg, #4e0000, #330000); color: #fc8181; } -.ship_COMPLETED { +.chip_COMPLETED { background: linear-gradient(180deg, #4e3300, #372400); color: #fcbb81; } -.ship_UNDELEGATION_REQUESTED { +.chip_UNDELEGATION_REQUESTED { color: rgb(252 248 129); background: linear-gradient(rgb(78 71 0), rgb(36 39 0)); } -.ship_CANCELED { +.chip_CANCELED { color: rgb(219 219 219); background: linear-gradient(rgb(59 59 59), rgb(36 36 36)); } -.ship_PROPOSED { +.chip_PROPOSED { color: rgb(57 218 248); background: linear-gradient(rgb(20 66 59), rgb(11 36 33)); } -.ship_DELEGATION_UI { +.chip_DELEGATION_UI { background: linear-gradient(180deg, #1e1b37, #131123); color: #8c81fc; } -.ship_MEW_WALLET { +.chip_MEW_WALLET { background: linear-gradient(180deg, #144348, #0c2326); color: #4bd9e9; } -.ship_ACTIVATE { +.chip_ACTIVATE { background: linear-gradient(180deg, #101635, #0a0e23); color: #6f82f4; } -.ship_PORTAL { +.chip_PORTAL { background: linear-gradient(180deg, #221d3c, #151225); color: #9681fc; } -.ship_SELF { +.chip_SELF { background: linear-gradient(180deg, #4e3300, #372400); color: #fcbb81; } -.ship_OTHER { +.chip_OTHER { background: linear-gradient(180deg, #3e1f3f, #2b152b); color: #f681fc; } -.ship_ETHERSCAN { +.chip_ETHERSCAN { background: linear-gradient(180deg, #1f203f, #15172b); color: #8199fc; } -.shipFee { +.chipFee { margin-right: 5px; background: linear-gradient(180deg, #1e4e00, #163800); @@ -904,7 +904,7 @@ input[type=number] { } } -.shipId { +.chipId { margin-right: 5px; background: linear-gradient(180deg, #333333, #212121); border-radius: 20px; @@ -926,7 +926,7 @@ input[type=number] { background-color: #da3a34 !important; } -.shipNodes { +.chipNodes { margin-right: 5px; background: linear-gradient(180deg, #301f35, #1f1322); border-radius: 20px; @@ -937,7 +937,7 @@ input[type=number] { } } -.shipAddress { +.chipAddress { margin-right: 5px; background: #1f3533; border-radius: 20px; diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index fefe47d5..b26b931b 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -128,7 +128,7 @@ export default function SkDrawer() { -
+

NEW

diff --git a/src/components/CategoryBadge.tsx b/src/components/CategoryBadge.tsx index e94c1f27..5c3235a1 100644 --- a/src/components/CategoryBadge.tsx +++ b/src/components/CategoryBadge.tsx @@ -44,7 +44,7 @@ import AgricultureRoundedIcon from '@mui/icons-material/AgricultureRounded' import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded' import PhotoCameraRoundedIcon from '@mui/icons-material/PhotoCameraRounded' -import Ship from './Ship' +import Chip from './Chip' export const CATEGORY_ICON: any = { hubs: , @@ -91,7 +91,7 @@ export default function CategoryBadge(props: { return (
- +
) } diff --git a/src/components/CategorySection.tsx b/src/components/CategorySection.tsx deleted file mode 100644 index 53c6654b..00000000 --- a/src/components/CategorySection.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * SKALE portal - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * @file CategorySection.tsx - * @copyright SKALE Labs 2022-Present - */ - -import { getChainAlias } from '@skalenetwork/metaport' -import { type types } from '@/core' - -import Box from '@mui/material/Box' -import Grid from '@mui/material/Grid' - -import ChainCard from './ChainCard' - -export default function CategorySection(props: { - schains: any - category: string - skaleNetwork: types.SkaleNetwork - chainsMeta: types.ChainsMetadataMap - metrics?: types.IMetrics | null -}) { - if (!props.schains || props.schains.length === 0) return - const schains = props.schains.sort((a: types.ISChain, b: types.ISChain) => { - const aliasA = getChainAlias(props.skaleNetwork, a.name) - const aliasB = getChainAlias(props.skaleNetwork, b.name) - return aliasA.localeCompare(aliasB) - }) - const isHub = props.category === 'hubs' - return ( -
- - - {schains.map((schain: types.ISChain) => ( - - - - ))} - - -
- ) -} diff --git a/src/components/Ship.tsx b/src/components/Chip.tsx similarity index 70% rename from src/components/Ship.tsx rename to src/components/Chip.tsx index d4a4e097..cf97eefa 100644 --- a/src/components/Ship.tsx +++ b/src/components/Chip.tsx @@ -16,13 +16,13 @@ * along with this program. If not, see . */ /** - * @file Ship.tsx + * @file Chip.tsx * @copyright SKALE Labs 2024-Present */ import { cls, cmn } from '@skalenetwork/metaport' -const Ship: React.FC<{ +const Chip: React.FC<{ label: string icon?: React.ReactNode onClick?: () => void @@ -30,7 +30,7 @@ const Ship: React.FC<{ }> = ({ label, icon, onClick, className }) => { return (
= ({}) => { - return +export const ChipTrending: React.FC<{}> = ({}) => { + return } -export const ShipNew: React.FC<{}> = ({}) => { - return +export const ChipNew: React.FC<{}> = ({}) => { + return } -export const ShipPreTge: React.FC<{}> = ({}) => { - return +export const ChipPreTge: React.FC<{}> = ({}) => { + return } -export default Ship +export default Chip diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index bb00f0c3..fde9a633 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -62,7 +62,7 @@ import { getRpcUrl, getChainId, HTTPS_PREFIX, getChainDescription } from '../cor import { getExplorerUrl } from '../core/explorer' import { formatNumber } from '../core/timeHelper' import ChainTabsSection from './chains/tabs/ChainTabsSection' -import CategoriesChips from './ecosystem/CategoriesShips' +import CategoriesChips from './ecosystem/CategoriesChips' export default function SchainDetails(props: { schainName: string diff --git a/src/components/chains/ChainActions.tsx b/src/components/chains/ChainActions.tsx index e7f1e4fa..367e7f3d 100644 --- a/src/components/chains/ChainActions.tsx +++ b/src/components/chains/ChainActions.tsx @@ -30,7 +30,7 @@ import ViewInArRoundedIcon from '@mui/icons-material/ViewInArRounded' import { getExplorerUrl } from '../../core/explorer' interface ChainActionsProps { - chainMeta: types.ChainMetadata + chainMeta?: types.ChainMetadata schainName: string skaleNetwork: types.SkaleNetwork className?: string @@ -47,7 +47,7 @@ const ChainActions: React.FC = ({ return (
- {chainMeta.url && ( + {chainMeta && chainMeta.url && (
-
- } - /> -
+ {chainMeta && ( +
+ } + /> +
+ )}

{getChainAlias(chainsMeta, schain.name)}

- + - + diff --git a/src/components/chains/ChainsSection.tsx b/src/components/chains/ChainsSection.tsx index ad41ead5..151cbca9 100644 --- a/src/components/chains/ChainsSection.tsx +++ b/src/components/chains/ChainsSection.tsx @@ -52,7 +52,6 @@ const ChainsSection: React.FC = ({ }) => { const gridSize = size === 'lg' ? { xs: 12, md: 6 } : { xs: 12, md: 4 } - // Sort schains based on chain alias const sortedSchains = [...schains].sort((a, b) => { const aliasA = getChainAlias(chainsMeta, a.name).toLowerCase() const aliasB = getChainAlias(chainsMeta, b.name).toLowerCase() diff --git a/src/components/chains/HubTile.tsx b/src/components/chains/HubTile.tsx index 5091ccc3..407c4517 100644 --- a/src/components/chains/HubTile.tsx +++ b/src/components/chains/HubTile.tsx @@ -97,7 +97,7 @@ export default function HubTile(props: {
{props.isXs || !props.showStats ? null : ( -
+

{schainMetrics diff --git a/src/components/chains/tabs/DeveloperInfo.tsx b/src/components/chains/tabs/DeveloperInfo.tsx index 263b3c89..af500302 100644 --- a/src/components/chains/tabs/DeveloperInfo.tsx +++ b/src/components/chains/tabs/DeveloperInfo.tsx @@ -52,11 +52,6 @@ export default function DeveloperInfo(props: { return ( - {/* } - className={cls(cmn.mbott20)} - /> */} diff --git a/src/components/delegation/Delegation.tsx b/src/components/delegation/Delegation.tsx index 375b7c30..4bac4740 100644 --- a/src/components/delegation/Delegation.tsx +++ b/src/components/delegation/Delegation.tsx @@ -139,7 +139,7 @@ export default function Delegation(props: {

-
+

{props.delegation.state.replace(/_/g, ' ')}

@@ -150,7 +150,7 @@ export default function Delegation(props: {
-
+

{source}

diff --git a/src/components/delegation/ValidatorCard.tsx b/src/components/delegation/ValidatorCard.tsx index 28eb5ca1..414d3f86 100644 --- a/src/components/delegation/ValidatorCard.tsx +++ b/src/components/delegation/ValidatorCard.tsx @@ -106,16 +106,16 @@ export default function ValidatorCard(props: {
-
+

{Number(props.validator.feeRate) / 10}% fee

-
+

ID: {props.validator.id}

{size === 'lg' ? ( -
+

Nodes: {props.validator.linkedNodes}

@@ -125,7 +125,7 @@ export default function ValidatorCard(props: {
{size !== 'lg' && ( -
+

Min: {minDelegation} SKL

@@ -134,7 +134,7 @@ export default function ValidatorCard(props: { )} {size === 'lg' && ( -
+

Address: {props.validator.validatorAddress}

diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 5aa628fb..42db25f0 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -31,9 +31,9 @@ import { getChainShortAlias } from '../../core/chain' import { chainBg, getChainAlias } from '../../core/metadata' import CollapsibleDescription from '../CollapsibleDescription' -import AppCategoriesChips from './CategoriesShips' +import AppCategoriesChips from './CategoriesChips' import SocialButtons from './Socials' -import { ShipTrending, ShipNew, ShipPreTge } from '../Ship' +import { ChipTrending, ChipNew, ChipPreTge } from '../Chip' export default function AppCard(props: { skaleNetwork: types.SkaleNetwork @@ -81,9 +81,9 @@ export default function AppCard(props: {

{getChainAlias(props.chainsMeta, props.schainName, props.appName)}

- {props.isTrending && } - {props.isNew && } - {appMeta.tags?.includes('pretge') && } + {props.isTrending && } + {props.isNew && } + {appMeta.tags?.includes('pretge') && }
diff --git a/src/components/ecosystem/CategoriesShips.tsx b/src/components/ecosystem/CategoriesChips.tsx similarity index 96% rename from src/components/ecosystem/CategoriesShips.tsx rename to src/components/ecosystem/CategoriesChips.tsx index aabe9e16..ce49d99d 100644 --- a/src/components/ecosystem/CategoriesShips.tsx +++ b/src/components/ecosystem/CategoriesChips.tsx @@ -17,7 +17,7 @@ */ /** - * @file CategoriesShips.tsx + * @file CategoriesChips.tsx * @copyright SKALE Labs 2024-Present */ @@ -25,7 +25,7 @@ import React, { useMemo, useState } from 'react' import { Box } from '@mui/material' import { type types } from '@/core' import { categories as categoriesData } from '../../core/ecosystem/categories' -import Ship from '../Ship' +import Chip from '../Chip' import { CategoryIcons } from './CategoryIcons' interface CategoriesChipsProps { @@ -53,7 +53,7 @@ const CategoriesChips: React.FC = ({ categories, className if (Array.isArray(categories)) { return categories.map((categoryTag) => ( - } @@ -61,14 +61,14 @@ const CategoriesChips: React.FC = ({ categories, className )) } else { return Object.entries(categories).flatMap(([categoryTag, subcategories]) => [ - } />, ...(Array.isArray(subcategories) ? subcategories.map((subTag) => ( - } @@ -88,7 +88,7 @@ const CategoriesChips: React.FC = ({ categories, className {visibleChips} {remainingChips > 0 && ( - setExpanded(!expanded)} /> diff --git a/src/components/ecosystem/CategoryIcons.tsx b/src/components/ecosystem/CategoryIcons.tsx index 6a3405dd..1c0afd15 100644 --- a/src/components/ecosystem/CategoryIcons.tsx +++ b/src/components/ecosystem/CategoryIcons.tsx @@ -39,7 +39,6 @@ import { PeopleOutlined, BuildOutlined, AccountBalanceWalletOutlined, - SportsKabaddiOutlined, CasinoOutlined, StyleOutlined, BeachAccessOutlined, @@ -51,7 +50,7 @@ import { ExtensionOutlined, DirectionsCarOutlined, AutoStoriesOutlined, - BedroomChildOutlined, + WallpaperOutlined, PsychologyOutlined, SportsBaseballOutlined, PrecisionManufacturingOutlined, @@ -62,7 +61,8 @@ import { CandlestickChartRounded, JoinRightRounded, PhoneIphoneOutlined, - DiamondOutlined + DiamondOutlined, + SailingOutlined } from '@mui/icons-material' export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { @@ -106,7 +106,7 @@ export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { // Gaming subcategories case 'action-adventure': - return + return case 'battle-royale': return case 'cards_deck-building': @@ -134,7 +134,7 @@ export const CategoryIcons: React.FC<{ category: string }> = ({ category }) => { case 'rpg': return case 'sandbox': - return + return case 'shooter': return case 'simulation': diff --git a/src/pages/App.tsx b/src/pages/App.tsx index b4e5b2f8..92b2a29c 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -56,11 +56,11 @@ import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' import { MAINNET_CHAIN_LOGOS, MAX_APPS_DEFAULT, OFFCHAIN_APP } from '../core/constants' import SocialButtons from '../components/ecosystem/Socials' -import AppCategoriesChips from '../components/ecosystem/CategoriesShips' +import AppCategoriesChips from '../components/ecosystem/CategoriesChips' import { useLikedApps } from '../LikedAppsContext' import { useAuth } from '../AuthContext' import ErrorTile from '../components/ErrorTile' -import { ShipNew, ShipPreTge, ShipTrending } from '../components/Ship' +import { ChipNew, ChipPreTge, ChipTrending } from '../components/Chip' import { getRecentApps, isNewApp } from '../core/ecosystem/utils' export default function App(props: { @@ -215,9 +215,9 @@ export default function App(props: {

{appAlias}

- {trendingAppIds.includes(appId) && } - {isNew && } - {appMeta.tags?.includes('pretge') && } + {trendingAppIds.includes(appId) && } + {isNew && } + {appMeta.tags?.includes('pretge') && }
diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index ffbad22b..656030c2 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -34,9 +34,11 @@ import CircularProgress from '@mui/material/CircularProgress' import StarRoundedIcon from '@mui/icons-material/StarRounded' import HubRoundedIcon from '@mui/icons-material/HubRounded' +import CategoryRoundedIcon from '@mui/icons-material/CategoryRounded' import ChainsSection from '../components/chains/ChainsSection' import { META_TAGS } from '../core/meta' +import { MAINNET_CHAIN_NAME } from '../core/constants' export default function Chains(props: { loadData: () => Promise @@ -48,12 +50,24 @@ export default function Chains(props: { }) { const [_, setIntervalId] = useState() + const network = props.mpc.config.skaleNetwork + useEffect(() => { props.loadData() const intervalId = setInterval(props.loadData, 10000) setIntervalId(intervalId) }, []) + const appChains = props.schains.filter( + (schain) => + props.chainsMeta[schain.name] && + (!props.chainsMeta[schain.name].apps || + (props.chainsMeta[schain.name].apps && + Object.keys(props.chainsMeta[schain.name].apps!).length === 1)) + ) + + const otherChains = props.schains.filter((schain) => !props.chainsMeta[schain.name]) + if (props.schains.length === 0) { return (
@@ -95,25 +109,32 @@ export default function Chains(props: { )} chainsMeta={props.chainsMeta} metrics={props.metrics} - skaleNetwork={props.mpc.config.skaleNetwork} + skaleNetwork={network} size="lg" icon={} /> - - props.chainsMeta[schain.name] && - (!props.chainsMeta[schain.name].apps || - (!props.chainsMeta[schain.name].apps && - Object.keys(props.chainsMeta[schain.name].apps!).length === 1)) - )} - chainsMeta={props.chainsMeta} - metrics={props.metrics} - skaleNetwork={props.mpc.config.skaleNetwork} - size="md" - icon={} - /> + {appChains.length !== 0 && ( + } + /> + )} + {network !== MAINNET_CHAIN_NAME && otherChains.length !== 0 && ( + } + /> + )} ) diff --git a/src/styles/ship.scss b/src/styles/chip.scss similarity index 100% rename from src/styles/ship.scss rename to src/styles/chip.scss diff --git a/vercel.json b/vercel.json index a4d981c7..21dec244 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "headers": [ { "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" + "value": "default-src 'none'; script-src 'self' 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; connect-src 'self' https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" }, { "key": "X-Content-Type-Options", From 40c120c26b2003a4e853a634067afaac75d16647 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 20:47:57 +0100 Subject: [PATCH 36/39] Add DISABLE_TRANSAK option --- src/core/constants.ts | 1 + src/pages/Onramp.tsx | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index 478c5b0f..9554ed2f 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -69,6 +69,7 @@ export const BALANCE_UPDATE_INTERVAL_MS = _BALANCE_UPDATE_INTERVAL_SECONDS * 100 export const TRANSAK_STAGING_ENV = import.meta.env.VITE_TRANSAK_STAGING_ENV === 'true' export const TRANSAK_API_KEY = import.meta.env.VITE_TRANSAK_API_KEY +export const DISABLE_TRANSAK = import.meta.env.VITE_DISABLE_TRANSAK === 'true' export const DAPP_RADAR_BASE_URL = 'https://dappradar.com/dapp/' diff --git a/src/pages/Onramp.tsx b/src/pages/Onramp.tsx index 103d1998..3c2239e1 100644 --- a/src/pages/Onramp.tsx +++ b/src/pages/Onramp.tsx @@ -39,7 +39,12 @@ import TokenBalanceTile from '../components/TokenBalanceTile' import ConnectWallet from '../components/ConnectWallet' import Message from '../components/Message' import { getPaymasterChain } from '../core/paymaster' -import { MAINNET_CHAIN_NAME, TRANSAK_STAGING_ENV, TRANSAK_API_KEY } from '../core/constants' +import { + MAINNET_CHAIN_NAME, + TRANSAK_STAGING_ENV, + TRANSAK_API_KEY, + DISABLE_TRANSAK +} from '../core/constants' import Tile from '../components/Tile' const MOUNT_ID = 'transakMount' @@ -52,6 +57,19 @@ export default function Onramp(props: { mpc: MetaportCore }) { const chain = getPaymasterChain(props.mpc.config.skaleNetwork) const isProd = props.mpc.config.skaleNetwork === MAINNET_CHAIN_NAME && !TRANSAK_STAGING_ENV + if (DISABLE_TRANSAK) + return ( + + } + color="warning" + className={cls(cmn.mtop20)} + /> + + ) + if (!chain) return ( From d185477b34bcf0706ebe716077a3ecf546ebe0b5 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 21:05:35 +0100 Subject: [PATCH 37/39] Update metaport, update submodules, remove staging --- bun.lockb | Bin 586922 -> 586922 bytes package.json | 2 +- packages/core/src/types/index.ts | 2 +- skale-network | 2 +- src/components/NetworkSwitch.tsx | 4 +--- src/core/constants.ts | 1 - src/data/contractsMeta.ts | 6 ------ 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/bun.lockb b/bun.lockb index 6c37e5664cf56facbfc3a3c10c268b06a9598f2c..f831b792f521341f7ca5c6a85e19dc27d2e434c5 100755 GIT binary patch delta 189 zcmV;u07Cz&=_9J?Bakj27RxpzNkS;!YM*2Ly~&A6Sd^`=!d+izWO4%S2oGT zBiP(YnMoYf>d?682|n@`D{hjNFl}rMMl}rPNl}rSOl}rV< rl}rW_)B!WMs2~SVI+p=q2OO8MR|gK4AzKF`0W*h0TL-s9TL@;E3zJt; delta 190 zcmV;v073t%=_9J?Bakj2ZZXeM$dSPD^b$E)N3rx4vr0YS_F!yfH`zqrlMbrKu}<2E z2!Y}`!C5fTW0P>08G}iQw@HZs48uSm6UZIYl*3)c2V#pB8H`1VxUfPpA}v<3Ok3FT zC62Z(^tU+mCtuLYwmBcO2_oE8Ep^NlFJVEeAg)@Q6t%C1l}rMMl}rPNl}rSOl}rV< sl}rW_)B!TLs2~SVIsr15k*o(DmVgKhmmymRA^|doL|X^9L|X`EncL-1g#Z8m diff --git a/package.json b/package.json index 012e5550..051334fe 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@mdx-js/rollup": "^2.3.0", "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", - "@skalenetwork/metaport": "3.0.0-develop.2", + "@skalenetwork/metaport": "3.0.0-develop.3", "@skalenetwork/skale-contracts-ethers-v6": "1.0.1", "@transak/transak-sdk": "^3.1.1", "@types/react-copy-to-clipboard": "^5.0.4", diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 9c910876..6518e5cc 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -35,7 +35,7 @@ export * as staking from './staking' export type AddressType = `0x${string}` export type Size = 'xs' | 'sm' | 'md' | 'lg' -export type SkaleNetwork = 'mainnet' | 'staging' | 'legacy' | 'regression' | 'testnet' +export type SkaleNetwork = 'mainnet' | 'legacy' | 'regression' | 'testnet' export type TSChainArray = [ string, diff --git a/skale-network b/skale-network index f9362124..2c1f37b5 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit f93621243a822d34e5a44c2cc90c197d42f4894e +Subproject commit 2c1f37b5ae24061391241dc5fa5d692b2afecce4 diff --git a/src/components/NetworkSwitch.tsx b/src/components/NetworkSwitch.tsx index f746f05e..68a90214 100644 --- a/src/components/NetworkSwitch.tsx +++ b/src/components/NetworkSwitch.tsx @@ -57,9 +57,7 @@ export default function NetworkSwitch(props: { mpc: MetaportCore }) { className={cls('mp__btnConnect', styles.paperGrey, cmn.pPrim, cmn.flex, cmn.cap)} > - {props.mpc.config.skaleNetwork === 'staging' - ? 'testnet' - : props.mpc.config.skaleNetwork} + {props.mpc.config.skaleNetwork} diff --git a/src/core/constants.ts b/src/core/constants.ts index f927efa0..6244b33c 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -75,7 +75,6 @@ export const DAPP_RADAR_BASE_URL = 'https://dappradar.com/dapp/' export const STATS_API: { [key in types.SkaleNetwork]: string | null } = { mainnet: 'https://stats.explorer.mainnet.skalenodes.com/v2/stats/', testnet: null, - staging: null, legacy: null, regression: null } diff --git a/src/data/contractsMeta.ts b/src/data/contractsMeta.ts index 57ed669c..d7603064 100644 --- a/src/data/contractsMeta.ts +++ b/src/data/contractsMeta.ts @@ -27,12 +27,6 @@ export const CONTRACTS_META: { [key in types.SkaleNetwork]: any } = { mainnet: { auto: true }, - staging: { - auto: false, - manager: '', - allocator: '', - grants: null - }, legacy: { auto: false, manager: '0x27C393Cd6CBD071E5F5F2227a915d3fF3650aeaE', From dbd43f754108e438f5cd3936a507d57dcf5103ea Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 21:15:02 +0100 Subject: [PATCH 38/39] Update constants.ts --- src/core/constants.ts | 4 ---- src/core/paymaster.ts | 2 +- src/data/contractsMeta.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index e2eaf8bd..707df7bf 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -81,7 +81,6 @@ export const STATS_API: { [key in types.SkaleNetwork]: string | null } = { } export const BASE_METADATA_URL = -<<<<<<< HEAD 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' export const MAX_APPS_DEFAULT = 12 @@ -102,6 +101,3 @@ export const SKALE_SOCIAL_LINKS = { swell: 'https://swell.skale.space/', website: 'https://skale.space/' } -======= - 'https://raw.githubusercontent.com/skalenetwork/skale-network/old-main/metadata/' ->>>>>>> 73e5b7c005a37263b2863abc80d4b67579c32008 diff --git a/src/core/paymaster.ts b/src/core/paymaster.ts index 678535e0..b629c199 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ /** - * @file constants.ts + * @file paymaster.ts * @copyright SKALE Labs 2022-Present */ diff --git a/src/data/contractsMeta.ts b/src/data/contractsMeta.ts index d7603064..a031c8cd 100644 --- a/src/data/contractsMeta.ts +++ b/src/data/contractsMeta.ts @@ -17,7 +17,7 @@ */ /** - * @file constants.ts + * @file contractsMeta.ts * @copyright SKALE Labs 2022-Present */ From b4a89b0a2320fefefb8ff8e25faf402a3b0f1fde Mon Sep 17 00:00:00 2001 From: Dmytro Date: Mon, 19 Aug 2024 21:21:21 +0100 Subject: [PATCH 39/39] Update css class --- src/App.scss | 2 +- src/SkDrawer.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.scss b/src/App.scss index 398f879c..1e2baa30 100644 --- a/src/App.scss +++ b/src/App.scss @@ -729,7 +729,7 @@ input[type=number] { /* Firefox */ } -.ChipNew { +.chipNew { margin-right: 5px; background: #93B8EC; border-radius: 20px; diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index b26b931b..2240bd67 100644 --- a/src/SkDrawer.tsx +++ b/src/SkDrawer.tsx @@ -128,7 +128,7 @@ export default function SkDrawer() { -
+

NEW