diff --git a/.gitignore b/.gitignore index 49ee909c..a589be2f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,7 @@ src/metadata/faucet.json public/sitemap.xml public/robots.txt + +packages/*/node_modules +packages/*/dist tools/* \ 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 32bbc234..f831b792 100755 Binary files a/bun.lockb and b/bun.lockb differ 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/mainnet.ts b/config/mainnet.ts index a2553ece..27d1de56 100644 --- a/config/mainnet.ts +++ b/config/mainnet.ts @@ -3,7 +3,9 @@ import { type interfaces } from '@skalenetwork/metaport' export const METAPORT_CONFIG: interfaces.MetaportConfig = { theme: { mode: 'dark', - vibrant: true + vibrant: true, + primary: '#93B8EC', + background: '#000000', }, mainnetEndpoint: 'https://cloudflare-eth.com/', skaleNetwork: "mainnet", @@ -109,6 +111,9 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { "adorable-quaint-bellatrix": {}, "honorable-steel-rasalhague": { hub: "elated-tan-skat" + }, + "green-giddy-denebola": { + hub: "elated-tan-skat" } } } @@ -237,6 +242,9 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { }, 'honorable-steel-rasalhague': { wrapper: '0xa5274efA35EbeFF47C1510529D9a8812F95F5735' + }, + 'green-giddy-denebola': { + wrapper: '0xa5274efA35EbeFF47C1510529D9a8812F95F5735' } } } @@ -398,6 +406,10 @@ export const METAPORT_CONFIG: interfaces.MetaportConfig = { mainnet: { clone: true, hub: 'elated-tan-skat' + }, + "green-giddy-denebola": { + clone: true, + hub: "elated-tan-skat" } } } @@ -474,6 +486,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", 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/package.json b/package.json index c9993f0a..051334fe 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,26 @@ { "name": "portal", "private": true, - "version": "2.3.0", + "version": "3.0.0", "type": "module", "scripts": { - "build-testnet": "NETWORK_NAME=testnet bash build.sh", - "build-mainnet": "NETWORK_NAME=mainnet bash build.sh", + "build:testnet": "NETWORK_NAME=testnet bash build.sh", + "build:mainnet": "NETWORK_NAME=mainnet bash build.sh", + "build:portal": "tsc && vite build", + "build:packages": "bun run build:core", + "build:core": "cd packages/core && bun install && bun run build", "dev": "vite", - "build": "tsc && vite build", "lint": "eslint ./src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "prettier": "prettier --write \"src/**/*.{ts,tsx,js,mdx}\"", "version": "node -e \"console.log(require('./package.json').version);\"" }, "dependencies": { + "@/core": "file:packages/core", "@mdx-js/rollup": "^2.3.0", "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", - "@skalenetwork/metaport": "3.0.0-develop.0", + "@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", @@ -31,7 +34,9 @@ "react-helmet": "^6.1.0", "react-jazzicon": "^1.0.4", "react-router-dom": "^6.15.0", - "react-transition-group": "^4.4.5" + "react-social-icons": "^6.17.0", + "react-transition-group": "^4.4.5", + "siwe": "^2.3.2" }, "devDependencies": { "@types/react": "^18.2.15", @@ -53,4 +58,4 @@ "vite": "^4.4.9", "vite-plugin-vercel": "^0.2.1" } -} +} \ No newline at end of file diff --git a/packages/core/bun.lockb b/packages/core/bun.lockb new file mode 100755 index 00000000..c8746163 Binary files /dev/null and b/packages/core/bun.lockb differ diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..c3f65fe2 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@/core", + "version": "1.0.0", + "description": "Core package for SKALE portal", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc && bun build ./src/index.ts --outdir ./dist --target node", + "type-check": "tsc --noEmit" + }, + "author": "SKALE Labs", + "devDependencies": { + "ethers": "*.*.*", + "typescript": "^4.9.5" + } +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..32fd0401 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,25 @@ +/** + * @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 + */ + +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..8c0d7d76 --- /dev/null +++ b/packages/core/src/types/ChainsMetadata.ts @@ -0,0 +1,82 @@ +/** + * @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 + categories: CategoriesMap + background?: string + gradientBackground?: string + description?: string + url?: string + apps?: AppMetadataMap +} + +export interface AppMetadata { + alias: string + gradientBackground?: string + description?: string + contracts?: string[] + social?: AppSocials + tags?: string[] + added?: number + categories: CategoriesMap +} +export interface AppWithChainAndName extends AppMetadata { + chain: string + appName: string +} +export interface AppSocials { + website?: string; + x?: string; + telegram?: string; + github?: string; + discord?: string; + swell?: string; + dappradar?: string; +} +export interface AppWithTimestamp { + chain: string + app: string + added: number +} + +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..6518e5cc --- /dev/null +++ b/packages/core/src/types/index.ts @@ -0,0 +1,168 @@ +/** + * @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, + AppWithTimestamp, + CategoriesMap, + AppWithChainAndName +} from './ChainsMetadata' +export * as staking from './staking' + +export type AddressType = `0x${string}` +export type Size = 'xs' | 'sm' | 'md' | 'lg' +export type SkaleNetwork = 'mainnet' | '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/src/components/ChainCategories.tsx b/packages/core/src/types/staking/Beneficiary.ts similarity index 53% rename from src/components/ChainCategories.tsx rename to packages/core/src/types/staking/Beneficiary.ts index e90683ec..bb59c56d 100644 --- a/src/components/ChainCategories.tsx +++ b/packages/core/src/types/staking/Beneficiary.ts @@ -15,30 +15,20 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - /** - * @file CategoryBadge.tsx + * @file Beneficiary.ts * @copyright SKALE Labs 2023-Present */ -import { cmn, cls } from '@skalenetwork/metaport' -import CategoryBadge, { isString } from './CategoryBadge' +import { AddressType } from ".." + +export type IBeneficiaryArray = [bigint, bigint, bigint, bigint, bigint, AddressType] -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) => ( - - )) - )} -
- ) +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/skale-network b/skale-network index ab24e67e..2c1f37b5 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit ab24e67e14097b395d219615d261cc4035b66a67 +Subproject commit 2c1f37b5ae24061391241dc5fa5d692b2afecce4 diff --git a/src/App.scss b/src/App.scss index 416b7130..1e2baa30 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,4 +1,6 @@ @import './variables'; +@import './styles/components'; +@import './styles/chip'; :root { background: black; @@ -66,7 +68,7 @@ body { font-family: inherit !important; } -.fullWidth { +.fullW { width: 100%; } @@ -256,14 +258,17 @@ body::-webkit-scrollbar { transform: scale(1.01); } -.br__tile { - // height: 150px; - border-radius: 25px !important; - border: 1px solid #171616 !important; +.border { + border: 1px solid $border-color !important; } -.border { - border: 1px solid #171616 !important; + +.borderLight { + border: 1px solid $border-color-light !important; +} + +.radius { + border-radius: $sk-border-radius !important; } .pageCard { @@ -366,6 +371,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; @@ -380,7 +396,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; @@ -433,11 +449,6 @@ body::-webkit-scrollbar { box-shadow: none !important; } - -.outlined { - background: rgba(41, 255, 148, 0.08) -} - .outlinedGray { background: rgb(126 126 126 / 15%); } @@ -584,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; } @@ -718,18 +729,42 @@ input[type=number] { /* Firefox */ } -.shipNew { +.chipNew { 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; } } -.shipXs { +.chipTrending { + background: linear-gradient(180deg, #e56d36, #D0602D) !important; + + p { + color: black !important + } +} + +.chipNewApp { + background: linear-gradient(180deg, #65a974, #508d5e) !important; + + p { + color: black !important + } +} + +.chipPreTge { + background: linear-gradient(180deg, #EBB84F, #bc923b) !important; + p { + color: black !important + } +} + +.chipXs { border-radius: 20px; padding: 3px 6px; @@ -739,7 +774,7 @@ input[type=number] { } } -.shipSm { +.chipSm { border-radius: 20px; padding: 6px 12px; @@ -749,16 +784,22 @@ input[type=number] { } } -.shipHot { - background: linear-gradient(180deg, #2b2b2b87, #0000008a); +.chipXs { + border-radius: 15px; + padding: 4px 8px; - p { - color: #dcdcdc !important + svg { + width: 12px; + height: 12px; } } +.skChip { + background: linear-gradient(180deg, rgb(52 52 52), rgb(31 31 31)); +} + -.ship { +.chip { border-radius: 20px; padding: 6px 12px; width: max-content; @@ -769,81 +810,89 @@ input[type=number] { } } -.ship_DELEGATED { +.chip_pretge { + background: linear-gradient(180deg, #E7B443, #b1882f); + color: #201808; +} + +.chip_new { + color: #09100b; + background: linear-gradient(#5C9B6A, #477a52); +} + +.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; - // background: linear-gradient(180deg, #103324, #091d14); - // color: #81fcd1; } -.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); @@ -855,7 +904,7 @@ input[type=number] { } } -.shipId { +.chipId { margin-right: 5px; background: linear-gradient(180deg, #333333, #212121); border-radius: 20px; @@ -866,9 +915,18 @@ input[type=number] { } } +.iconRed { + color: #da3a34 !important; + svg { + color: #da3a34 !important; + } +} +.btnRed { + background-color: #da3a34 !important; +} -.shipNodes { +.chipNodes { margin-right: 5px; background: linear-gradient(180deg, #301f35, #1f1322); border-radius: 20px; @@ -879,7 +937,7 @@ input[type=number] { } } -.shipAddress { +.chipAddress { margin-right: 5px; background: #1f3533; border-radius: 20px; @@ -892,11 +950,6 @@ input[type=number] { // md styling -a, -.a { - color: #71ffb8 !important; -} - .markdown { hr { border-color: $border-color; @@ -1186,14 +1239,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 { @@ -1230,16 +1280,47 @@ a, padding-bottom: 60px; } +.m5 { + margin: 5px; +} + .fwmobile { 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; + } +} + +.socialIconMd { + width: 40px !important; + height: 40px !important; + + svg { + width: 40px !important; + height: 40px !important; + } } .pSec { color: RGB(255 255 255 / 65%); + + svg { + fill: RGB(255 255 255 / 65%); + } + } .addressInput { @@ -1250,4 +1331,21 @@ a, .Mui-disabled { -webkit-text-fill-color: #ffffff; } +} + +.bgBlack { + background: black !important; +} + + +.bg { + background: $sk-bg !important; +} + +.bgPrim { + background: $sk-bg-prim !important; +} + +.hidden { + display: none; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7b39af98..f4cd46d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,8 @@ import { METAPORT_CONFIG } from './data/metaportConfig' import { createMuiTheme } from './core/themes' import { META_TAGS } from './core/meta' +import { AuthProvider } from './AuthContext' +import { LikedAppsProvider } from './LikedAppsContext' METAPORT_CONFIG.mainnetEndpoint = import.meta.env.VITE_MAINNET_ENDPOINT METAPORT_CONFIG.projectId = import.meta.env.VITE_WC_PROJECT_ID @@ -57,7 +59,11 @@ export default function App() { - + + + + + 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/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/LikedAppsContext.tsx b/src/LikedAppsContext.tsx new file mode 100644 index 00000000..1d6fb72f --- /dev/null +++ b/src/LikedAppsContext.tsx @@ -0,0 +1,167 @@ +/** + * @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' +import { API_URL, LIKES_REFRESH_INTERVAL, MAX_APPS_DEFAULT } from './core/constants' +import { types } from '@/core' + +interface AppLikes { + [appId: string]: number +} + +interface LikedAppsContextType { + likedApps: string[] + appLikes: AppLikes + isLoading: boolean + error: string | null + toggleLikedApp: (appId: string) => Promise + refreshLikedApps: () => Promise + getAppId: (chainName: string, appName: string) => string + getAppInfoById: (appId: string) => types.IAppId + getTrendingApps: () => string[] +} + +const LikedAppsContext = createContext(undefined) + +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() + 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 fetchAppLikes = useCallback(async () => { + try { + const response = await fetch(`${API_URL}/apps/all`) + 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', + 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])) + 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() + } else { + setLikedApps([]) + setIsLoading(false) + } + }, [isSignedIn, address, fetchLikedApps]) + + const getAppId = (chainName: string, appName: string) => `${chainName}--${appName}` + const getAppInfoById = (appId: string) => { + const [chain, app] = appId.split('--') + return { chain, app } + } + + const getTrendingApps = useCallback(() => { + return Object.entries(appLikes) + .sort(([, likesA], [, likesB]) => likesB - likesA) + .slice(0, MAX_APPS_DEFAULT) + .map(([appId]) => appId) + }, [appLikes]) + + 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/Router.tsx b/src/Router.tsx index da4fb317..e3a3be9a 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,7 @@ 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 +64,10 @@ 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 [stats, setStats] = useState(null) const [termsAccepted, setTermsAccepted] = useState(false) const [stakingTermsAccepted, setStakingTermsAccepted] = useState(false) @@ -77,7 +76,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 +97,7 @@ export default function Router() { }, []) useEffect(() => { - setCustomAddress((searchParams.get('_customAddress') as interfaces.AddressType) ?? undefined) + setCustomAddress((searchParams.get('_customAddress') as types.AddressType) ?? undefined) }, [location]) useEffect(() => { @@ -145,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) @@ -239,13 +237,12 @@ export default function Router() { } /> - } /> + } /> } /> @@ -278,6 +275,12 @@ export default function Router() { /> } /> + + } + /> + - } /> } /> } /> diff --git a/src/SkBottomNavigation.tsx b/src/SkBottomNavigation.tsx index a0066716..ea2bf2cf 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) @@ -41,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 ( @@ -69,8 +71,15 @@ export default function SkBottomNavigation() { }} /> } + onClick={() => { + navigate('/ecosystem') + }} + /> + } onClick={() => { navigate('/chains') }} diff --git a/src/SkDrawer.tsx b/src/SkDrawer.tsx index c33523ee..2240bd67 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' @@ -46,7 +47,7 @@ export default function SkDrawer() { - + @@ -59,7 +60,7 @@ export default function SkDrawer() {

Bridge

- + - + - + - + - + - + - + @@ -103,27 +104,38 @@ export default function SkDrawer() {

Network

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

NEW

- + - + - + - + diff --git a/src/_variables.scss b/src/_variables.scss index 3880aff4..3a275190 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -1,9 +1,11 @@ $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%); $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/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 . - */ - -/** - * @file AppChains.tsx - * @copyright SKALE Labs 2024-Present - */ - -import { cmn, cls, type interfaces, SkPaper, styles } from '@skalenetwork/metaport' - -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 - schains: any[] - metrics: IMetrics | null - chainsMeta: interfaces.ChainsMetadataMap - isXs: boolean -}) { - if (props.schains.length === 0) return - - return ( -
-
- -
- -
-

AppChains

-

- Apps and games hosted on dedicated SKALE Chains -

-
-
-
-
-
- props.chainsMeta[schain.name]) - : props.schains - } - metrics={props.metrics} - chainsMeta={props.chainsMeta} - /> -
-
-
-
- ) -} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 6e607766..e9f8cd11 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 ( @@ -32,14 +38,14 @@ export default function Breadcrumbs(props: { sections: BreadcrumbSection[]; clas {props.sections.map((section: BreadcrumbSection, index) => (
{section.url ? ( - - ) : ( - 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/CategoryBadge.tsx b/src/components/CategoryBadge.tsx index fdcddca1..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 { cmn, cls } from '@skalenetwork/metaport' +import Chip from './Chip' export const CATEGORY_ICON: any = { hubs: , @@ -76,20 +76,10 @@ export const CATEGORY_ICON: any = { Photos: } -export function getPrimaryCategory(category: string | string[] | undefined) { - if (!category) return 'other' - if (isString(category)) return category - if (isStringArray(category) && category.length !== 0) return category[0] -} - export function isString(value: any): value is string { return typeof value === 'string' } -function isStringArray(value: any): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === 'string') -} - export default function CategoryBadge(props: { category: string isXs: boolean @@ -100,15 +90,8 @@ export default function CategoryBadge(props: { } return ( -
- {getCategoryIcon(props.category)} -

{props.category}

+
+
) } diff --git a/src/components/CategorySection.tsx b/src/components/CategorySection.tsx deleted file mode 100644 index 4ab56c6c..00000000 --- a/src/components/CategorySection.tsx +++ /dev/null @@ -1,75 +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 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 -}) { - if (!props.schains || props.schains.length === 0) return - const schains = props.schains.sort((a: ISChain, b: 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: ISChain) => ( - - - - ))} - - -
- ) -} diff --git a/src/components/ChainCard.tsx b/src/components/ChainCard.tsx index fd95acc4..3773ad17 100644 --- a/src/components/ChainCard.tsx +++ b/src/components/ChainCard.tsx @@ -22,20 +22,20 @@ */ import { Link } from 'react-router-dom' -import { cmn, cls, chainBg, getChainAlias, type interfaces } from '@skalenetwork/metaport' +import { cmn, cls, chainBg, getChainAlias } 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 { ISChain } from '../core/types' import { getChainShortAlias } from '../core/chain' export default function ChainCard(props: { - skaleNetwork: interfaces.SkaleNetwork - schain: ISChain - chainsMeta: interfaces.ChainsMetadataMap + skaleNetwork: types.SkaleNetwork + schain: types.ISChain + chainsMeta: types.ChainsMetadataMap transactions?: number }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schain.name) @@ -60,9 +60,7 @@ export default function ChainCard(props: {
-
+
diff --git a/src/components/PageCard.tsx b/src/components/PageCard.tsx index 38778f12..9bb7757a 100644 --- a/src/components/PageCard.tsx +++ b/src/components/PageCard.tsx @@ -22,34 +22,31 @@ */ 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 }) { +export default function PageCard(props: { + name: string + icon: any + description: string + url?: string +}) { return ( -
-
- -
-
-
-
-

- {props.name} -

-
-

{props.description}

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

{props.name}

+

{props.description}

- -
-
+
+ +
+
+ + ) } 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/SchainDetails.tsx b/src/components/SchainDetails.tsx index d99472c3..fde9a633 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -30,14 +30,11 @@ import { styles, PROXY_ENDPOINTS, type MetaportCore, - SkPaper, - getChainAlias, - chainBg, - type interfaces + 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' @@ -53,24 +50,25 @@ 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' 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 { IChainMetrics, IStatsData } from '../core/types' import { formatNumber } from '../core/timeHelper' import ChainTabsSection from './chains/tabs/ChainTabsSection' +import CategoriesChips from './ecosystem/CategoriesChips' 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 @@ -90,7 +88,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', @@ -121,7 +119,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 @@ -146,56 +144,63 @@ export default function SchainDetails(props: { return (
+
+ , + url: '/chains' + }, + { + text: chainAlias, + icon: + } + ]} + /> +
+
SKALE Portal - {chainAlias} - - -
- , - url: '/chains' - }, - { - text: chainAlias, - icon: - } - ]} - /> -
-
-
- -
- - - - - -

{chainAlias}

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

{chainAlias}

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

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/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/ChainActions.tsx b/src/components/chains/ChainActions.tsx new file mode 100644 index 00000000..367e7f3d --- /dev/null +++ b/src/components/chains/ChainActions.tsx @@ -0,0 +1,78 @@ +/** + * @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 ChainActions.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { IconButton, Tooltip } from '@mui/material' +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 { getExplorerUrl } from '../../core/explorer' + +interface ChainActionsProps { + chainMeta?: types.ChainMetadata + schainName: string + skaleNetwork: types.SkaleNetwork + className?: string +} + +const ChainActions: React.FC = ({ + chainMeta, + schainName, + skaleNetwork, + className +}) => { + const explorerUrl = getExplorerUrl(skaleNetwork, schainName) + const isMd = false + + return ( +

+ {chainMeta && chainMeta.url && ( + + + + + + )} + + + + + +
+ ) +} + +export default ChainActions diff --git a/src/components/chains/ChainCard.tsx b/src/components/chains/ChainCard.tsx new file mode 100644 index 00000000..2f820818 --- /dev/null +++ b/src/components/chains/ChainCard.tsx @@ -0,0 +1,98 @@ +/** + * @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 ChainCard.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { Link } from 'react-router-dom' +import { cmn, cls, SkPaper } from '@skalenetwork/metaport' +import { type types } from '@/core' + +import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' + +import { MAINNET_CHAIN_LOGOS } from '../../core/constants' +import { getChainShortAlias } from '../../core/chain' +import { chainBg, getChainAlias } from '../../core/metadata' + +import ChainLogo from '../ChainLogo' +import CollapsibleDescription from '../CollapsibleDescription' +import ChainActions from './ChainActions' +import Chip from '../Chip' + +import { formatNumber } from '../../core/timeHelper' +import CategoriesChips from '../ecosystem/CategoriesChips' + +const ChainCard: React.FC<{ + skaleNetwork: types.SkaleNetwork + schain: types.ISChain + chainsMeta: types.ChainsMetadataMap + transactions: number | null +}> = ({ skaleNetwork, schain, chainsMeta, transactions }) => { + const shortAlias = getChainShortAlias(chainsMeta, schain.name) + const url = `/chains/${shortAlias}` + const chainMeta = chainsMeta[schain.name] + + return ( + + +
+
+
+ +
+
+
+
+ {chainMeta && ( +
+ } + /> +
+ )} +
+
+

+ {getChainAlias(chainsMeta, schain.name)} +

+
+ + + + +
+ ) +} + +export default ChainCard diff --git a/src/components/chains/ChainsSection.tsx b/src/components/chains/ChainsSection.tsx new file mode 100644 index 00000000..151cbca9 --- /dev/null +++ b/src/components/chains/ChainsSection.tsx @@ -0,0 +1,84 @@ +/** + * @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 ChainsSection.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { ReactElement } from 'react' +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' + +interface ChainsSectionProps { + name: string + schains: types.ISChain[] + chainsMeta: types.ChainsMetadataMap + metrics: types.IMetrics | null + skaleNetwork: types.SkaleNetwork + icon: ReactElement | undefined + size?: 'md' | 'lg' +} + +const ChainsSection: React.FC = ({ + name, + schains, + chainsMeta, + metrics, + skaleNetwork, + icon, + size = 'md' +}) => { + const gridSize = size === 'lg' ? { xs: 12, md: 6 } : { xs: 12, md: 4 } + + 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 ( +
+ + + {sortedSchains.map((schain) => ( + + + + ))} + +
+ ) +} + +export default ChainsSection diff --git a/src/components/chains/HubApps.tsx b/src/components/chains/HubApps.tsx index 28d7d172..aa0808b3 100644 --- a/src/components/chains/HubApps.tsx +++ b/src/components/chains/HubApps.tsx @@ -21,73 +21,42 @@ * @copyright SKALE Labs 2024-Present */ -import { useState, ReactElement } from 'react' -import { Grid, Tooltip } from '@mui/material' +import { ReactElement } from 'react' +import { type types } from '@/core' -import { cmn, cls, type interfaces } from '@skalenetwork/metaport' - -import { chainBg } from '../../core/metadata' +import { Grid } from '@mui/material' import { sortObjectByKeys } from '../../core/helper' -import AppCard from '../AppCard' +import AppCard from '../ecosystem/AppCard' export default function HubApps(props: { - skaleNetwork: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork schainName: string - chainsMeta: interfaces.ChainsMetadataMap - bg?: boolean - all?: boolean + chainsMeta: types.ChainsMetadataMap }) { - const [show, setShow] = useState(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/HubTile.tsx b/src/components/chains/HubTile.tsx index 3626d778..407c4517 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]) { @@ -65,7 +65,7 @@ export default function HubTile(props: { @@ -97,7 +97,7 @@ export default function HubTile(props: {
{props.isXs || !props.showStats ? null : ( -
+

{schainMetrics diff --git a/src/components/chains/tabs/ChainTabsSection.tsx b/src/components/chains/tabs/ChainTabsSection.tsx index 74c66580..1487af09 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' @@ -36,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 = [ { @@ -74,7 +74,7 @@ function CustomTabPanel(props: TabPanelProps) { export default function ChainTabsSection(props: { mpc: MetaportCore - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap schainName: string isXs: boolean }) { @@ -106,18 +106,11 @@ export default function ChainTabsSection(props: { currentTabs.unshift({ label: 'Apps', icon: }) currentTabsContent.unshift( - } - className={cls(cmn.mbott20)} - />

diff --git a/src/components/chains/tabs/DeveloperInfo.tsx b/src/components/chains/tabs/DeveloperInfo.tsx index 21b2662c..af500302 100644 --- a/src/components/chains/tabs/DeveloperInfo.tsx +++ b/src/components/chains/tabs/DeveloperInfo.tsx @@ -21,10 +21,10 @@ * @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' import CopySurface from '../../CopySurface' @@ -36,11 +36,10 @@ import { HTTPS_PREFIX, WSS_PREFIX } from '../../../core/chain' -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] @@ -53,11 +52,6 @@ export default function DeveloperInfo(props: { return ( - } - className={cls(cmn.mbott20)} - /> diff --git a/src/components/chains/tabs/Tabs.tsx b/src/components/chains/tabs/Tabs.tsx index 721e2f81..be952c6d 100644 --- a/src/components/chains/tabs/Tabs.tsx +++ b/src/components/chains/tabs/Tabs.tsx @@ -27,12 +27,14 @@ 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' +import { Button } from '@mui/material' export default function ChainTabs(props: { - chainMeta: interfaces.ChainMetadata + chainMeta: types.ChainMetadata handleChange: (event: React.SyntheticEvent, newValue: number) => void tabs: any[] tab: number @@ -43,11 +45,11 @@ export default function ChainTabs(props: {
{props.tabs.map((tab, index) => tab ? ( @@ -56,17 +58,17 @@ 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 )} - - } - iconPosition="start" - className={cls('btn', 'btnSm', cmn.mri10, cmn.mleft10, '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/delegation/Delegation.tsx b/src/components/delegation/Delegation.tsx index 4de08c42..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}

@@ -190,7 +190,7 @@ export default function Delegation(props: { loading={loading} text={loading ? 'Unstaking tokens' : 'Unstake tokens'} color="error" - className="fullWidth" + className="fullW" onClick={async () => { await props.unstake(delegationInfo) }} @@ -202,7 +202,7 @@ export default function Delegation(props: { loading={loading} text={loading ? 'Canceling staking request' : 'Cancel staking request'} color="warning" - className="fullWidth" + className="fullW" onClick={async () => { await props.cancelRequest(delegationInfo) }} 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" > - -
+ { - props.validator.acceptNewRequests ? props.setValidatorId(props.validator.id) : null - }} >
@@ -103,42 +106,43 @@ export default function ValidatorCard(props: {
-
+

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

-
+

ID: {props.validator.id}

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

Nodes: {props.validator.linkedNodes}

) : 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/components/ecosystem/AllApps.tsx b/src/components/ecosystem/AllApps.tsx new file mode 100644 index 00000000..2379b908 --- /dev/null +++ b/src/components/ecosystem/AllApps.tsx @@ -0,0 +1,66 @@ +/** + * @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 AllApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useMemo } from 'react' +import { type types } from '@/core' +import { useLikedApps } from '../../LikedAppsContext' +import AppCardV2 from './AppCardV2' +import { Grid } from '@mui/material' +import { isNewApp } from '../../core/ecosystem/utils' + +interface AllAppsProps { + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap + apps: types.AppWithChainAndName[] + newApps: types.AppWithTimestamp[] +} + +const AllApps: React.FC = ({ skaleNetwork, chainsMeta, apps, newApps }) => { + const { getTrendingApps, getAppId } = useLikedApps() + + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + + 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/AppCard.tsx b/src/components/ecosystem/AppCard.tsx similarity index 82% rename from src/components/AppCard.tsx rename to src/components/ecosystem/AppCard.tsx index df488fc9..a067c76b 100644 --- a/src/components/AppCard.tsx +++ b/src/components/ecosystem/AppCard.tsx @@ -22,30 +22,31 @@ */ 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' -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: interfaces.SkaleNetwork + skaleNetwork: types.SkaleNetwork schainName: string appName: string - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap transactions?: number }) { const shortAlias = getChainShortAlias(props.chainsMeta, props.schainName) - const url = `/chains/${shortAlias}/${props.appName}` + const url = `/ecosystem/${shortAlias}/${props.appName}` return (
@@ -67,7 +68,7 @@ export default function AppCard(props: { cmn.mbott10, 'br__tileBott', - 'fullWidth' + 'fullW' )} >
diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx new file mode 100644 index 00000000..42db25f0 --- /dev/null +++ b/src/components/ecosystem/AppCardV2.tsx @@ -0,0 +1,99 @@ +/** + * @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 AppCardV2.tsx + * @copyright SKALE Labs 2024-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, OFFCHAIN_APP } from '../../core/constants' +import { getChainShortAlias } from '../../core/chain' +import { chainBg, getChainAlias } from '../../core/metadata' + +import CollapsibleDescription from '../CollapsibleDescription' +import AppCategoriesChips from './CategoriesChips' +import SocialButtons from './Socials' +import { ChipTrending, ChipNew, ChipPreTge } from '../Chip' + +export default function AppCard(props: { + skaleNetwork: types.SkaleNetwork + schainName: string + appName: string + 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] + + if (!appMeta) return + + const appDescription = appMeta.description ?? 'No description' + + return ( + + +
+
+
+ +
+
+
+
+ {props.schainName !== OFFCHAIN_APP && ( + + )} +
+
+

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

+ {props.isTrending && } + {props.isNew && } + {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..9d6e59a4 --- /dev/null +++ b/src/components/ecosystem/Categories.tsx @@ -0,0 +1,230 @@ +/** + * @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 } from 'react' +import { cmn, cls } from '@skalenetwork/metaport' +import { filterCategories } from '../../core/ecosystem/utils' +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 { type Category } from '../../core/ecosystem/categories' +import SearchBar, { highlightMatch } from './SearchBar' +import SubcategoryList from './SubcategoryList' + +interface CategoryDisplayProps { + checkedItems: string[] + setCheckedItems: (items: string[]) => void + isXs?: boolean +} + +const CategoryDisplay: React.FC = ({ + checkedItems, + setCheckedItems, + isXs +}) => { + const [expandedItems, setExpandedItems] = useState>({}) + const [anchorEl, setAnchorEl] = useState(null) + const [searchTerm, setSearchTerm] = useState('') + const buttonRef = useRef(null) + + const filteredCategories = useMemo(() => filterCategories(searchTerm), [searchTerm]) + + const handleCheck = (category: string, subcategory: string | null = null): void => { + const newCheckedItems = [...checkedItems] + const itemToToggle = subcategory ? `${category}_${subcategory}` : category + + if (newCheckedItems.includes(itemToToggle)) { + const index = newCheckedItems.indexOf(itemToToggle) + newCheckedItems.splice(index, 1) + + 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 { + const subcategoriesToRemove = newCheckedItems.filter((item) => + item.startsWith(`${category}_`) + ) + subcategoriesToRemove.forEach((item) => { + const index = newCheckedItems.indexOf(item) + if (index !== -1) { + newCheckedItems.splice(index, 1) + } + }) + newCheckedItems.push(category) + setExpandedItems((prev) => ({ ...prev, [category]: true })) + } + } + + setCheckedItems(newCheckedItems) + } + + 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 => { + setSearchTerm(event.target.value) + } + + const handleClearSearch = (): void => { + setSearchTerm('') + setExpandedItems({}) + } + + 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 + 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) > 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/CategoriesChips.tsx b/src/components/ecosystem/CategoriesChips.tsx new file mode 100644 index 00000000..ce49d99d --- /dev/null +++ b/src/components/ecosystem/CategoriesChips.tsx @@ -0,0 +1,100 @@ +/** + * @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 CategoriesChips.tsx + * @copyright SKALE Labs 2024-Present + */ + +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 Chip from '../Chip' +import { CategoryIcons } from './CategoryIcons' + +interface CategoriesChipsProps { + categories: string[] | types.CategoriesMap + className?: string +} + +const CategoriesChips: React.FC = ({ categories, className }) => { + const [expanded, setExpanded] = useState(false) + + const chips = useMemo(() => { + if (!categories) return [] + + const getCategoryName = (tag: string) => categoriesData[tag]?.name ?? tag + const getSubcategoryName = (categoryTag: string, subcategoryTag: string): string => { + const category = categoriesData[categoryTag] + if (!category) return subcategoryTag + if (Array.isArray(category.subcategories)) { + return category.subcategories.includes(subcategoryTag) ? subcategoryTag : subcategoryTag + } + return category.subcategories && typeof category.subcategories === 'object' + ? category.subcategories[subcategoryTag]?.name ?? subcategoryTag + : subcategoryTag + } + + 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 + + const visibleChips = expanded ? chips : chips.slice(0, 2) + const remainingChips = chips.length - 2 + + return ( + + {visibleChips} + {remainingChips > 0 && ( + setExpanded(!expanded)} + /> + )} + + ) +} + +export default CategoriesChips diff --git a/src/components/ecosystem/CategoryCard.tsx b/src/components/ecosystem/CategoryCard.tsx new file mode 100644 index 00000000..d1131c02 --- /dev/null +++ b/src/components/ecosystem/CategoryCard.tsx @@ -0,0 +1,55 @@ +/** + * @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 CategoryCard.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { Link } from 'react-router-dom' +import { cmn, cls, SkPaper } from '@skalenetwork/metaport' + +interface CategoryCardProps { + categoryName: string + fullName: string + projectCount: number +} + +const CategoryCard: React.FC = ({ categoryName, fullName, projectCount }) => { + return ( + + +
+
+
+

+ {fullName} +

+
+

+ {projectCount} project{projectCount !== 1 ? 's' : ''} +

+
+
+
+ + ) +} + +export default CategoryCard diff --git a/src/components/ecosystem/CategoryCardsGrid.tsx b/src/components/ecosystem/CategoryCardsGrid.tsx new file mode 100644 index 00000000..1c470258 --- /dev/null +++ b/src/components/ecosystem/CategoryCardsGrid.tsx @@ -0,0 +1,78 @@ +/** + * @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 CategoryCardsGrid.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useMemo } from 'react' +import { Grid } from '@mui/material' +import { type types } from '@/core' + +import { categories } from '../../core/ecosystem/categories' +import CategoryCard from './CategoryCard' + +interface CategoryCardsGridProps { + chainsMeta: types.ChainsMetadataMap + maxCategories?: number +} + +interface CategoryData { + name: string + fullName: string + projectCount: number +} + +const CategoryCardsGrid: React.FC = ({ chainsMeta, maxCategories = 8 }) => { + const topCategories = useMemo(() => { + const getCategoryProjectCount = (categoryName: string): number => { + let count = 0 + Object.values(chainsMeta).forEach((chain) => { + if (chain.apps) { + Object.values(chain.apps).forEach((app) => { + if (app.categories && app.categories[categoryName] !== undefined) { + count++ + } + }) + } + }) + return count + } + + const categoryData: CategoryData[] = Object.entries(categories).map(([name, data]) => ({ + name, + fullName: data.name, + projectCount: getCategoryProjectCount(name) + })) + + return categoryData.sort((a, b) => b.projectCount - a.projectCount).slice(0, maxCategories) + }, [chainsMeta, maxCategories]) + + return ( + + {topCategories.map(({ name, fullName, projectCount }) => ( + + + + ))} + + ) +} + +export default CategoryCardsGrid diff --git a/src/components/ecosystem/CategoryIcons.tsx b/src/components/ecosystem/CategoryIcons.tsx new file mode 100644 index 00000000..1c0afd15 --- /dev/null +++ b/src/components/ecosystem/CategoryIcons.tsx @@ -0,0 +1,159 @@ +/** + * @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, + TheaterComedyOutlined, + ExploreOutlined, + SportsEsportsOutlined, + HubOutlined, + MemoryOutlined, + FilterFramesOutlined, + VisibilityOutlined, + MoreHorizOutlined, + HandshakeOutlined, + SecurityOutlined, + PeopleOutlined, + BuildOutlined, + AccountBalanceWalletOutlined, + CasinoOutlined, + StyleOutlined, + BeachAccessOutlined, + TvOutlined, + SportsHandballOutlined, + VrpanoOutlined, + ComputerOutlined, + GamesOutlined, + ExtensionOutlined, + DirectionsCarOutlined, + AutoStoriesOutlined, + WallpaperOutlined, + PsychologyOutlined, + SportsBaseballOutlined, + PrecisionManufacturingOutlined, + AutoAwesomeOutlined, + HikingRounded, + FlagRounded, + FlareRounded, + CandlestickChartRounded, + JoinRightRounded, + PhoneIphoneOutlined, + DiamondOutlined, + SailingOutlined +} 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 + case 'web3': + 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 + case 'strategy': + return + + // DeFi subcategories + case 'custody': + return + case 'dex': + return + case 'yield': + return + + // Default case + default: + return + } +} diff --git a/src/components/ecosystem/FavoriteApps.tsx b/src/components/ecosystem/FavoriteApps.tsx new file mode 100644 index 00000000..c6a1a98c --- /dev/null +++ b/src/components/ecosystem/FavoriteApps.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 FavoriteApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useEffect, useMemo } 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' + +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' +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, getAppInfoById, getTrendingApps } = useLikedApps() + const { isSignedIn } = useAuth() + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + + useEffect(() => { + if (isSignedIn) { + refreshLikedApps() + } + }, [isSignedIn, refreshLikedApps]) + + if (!isSignedIn) return + if (error) return
Error: {error}
+ + const appCards = likedApps.map((appId) => { + const { chain, app } = getAppInfoById(appId) + const isTrending = trendingAppIds.includes(appId) + const isNew = isNewApp({ chain, app }, props.newApps) + 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..f98479d4 --- /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/NewApps.tsx b/src/components/ecosystem/NewApps.tsx new file mode 100644 index 00000000..e8b53f42 --- /dev/null +++ b/src/components/ecosystem/NewApps.tsx @@ -0,0 +1,77 @@ +/** + * @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, { 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 }[] + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap + useCarousel?: boolean +} + +const NewApps: React.FC = ({ + newApps, + skaleNetwork, + chainsMeta, + useCarousel = false +}) => { + 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 {newApps.map(renderAppCard)} + } + + return ( + + {newApps.map((app) => ( + + {renderAppCard(app)} + + ))} + + ) +} + +export default NewApps 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..2e17688c --- /dev/null +++ b/src/components/ecosystem/SelectedCategories.tsx @@ -0,0 +1,122 @@ +/** + * @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 { categories } from '../../core/ecosystem/categories' + +interface SelectedCategoriesProps { + checkedItems: string[] + setCheckedItems: (items: string[]) => void + filteredAppsCount: number +} + +const CustomChipLabel: React.FC<{ category: string; subcategory?: string }> = ({ + category, + subcategory +}) => ( + +

+ {category} +

+ {subcategory && ( + <> + +

{subcategory}

+ + )} +
+) + +const SelectedCategories: React.FC = ({ + checkedItems, + setCheckedItems, + filteredAppsCount +}) => { + const handleDelete = (itemToDelete: string) => { + setCheckedItems(checkedItems.filter((item) => item !== itemToDelete)) + } + + const clearAll = () => { + setCheckedItems([]) + } + + const getCategoryName = (key: string): string => categories[key]?.name || key + + const getSubcategoryName = (categoryKey: string, subcategoryKey: string): string => { + const category = categories[categoryKey] + if ( + category && + typeof category.subcategories === 'object' && + !Array.isArray(category.subcategories) + ) { + return category.subcategories[subcategoryKey]?.name || subcategoryKey + } + return subcategoryKey + } + + if (checkedItems.length === 0) return null + + return ( + + {checkedItems.map((item) => { + const [category, subcategory] = item.split('_') + return ( + + } + onDelete={() => handleDelete(item)} + 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..87087e48 --- /dev/null +++ b/src/components/ecosystem/Socials.tsx @@ -0,0 +1,135 @@ +/** + * @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 { IconButton, Tooltip } from '@mui/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' +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 + chainName?: string + appName?: string + className?: string + size?: 'sm' | 'md' +} + +const SocialButtons: React.FC = ({ + social, + chainName, + appName, + className, + size = 'sm' +}) => { + const isMd = size === 'md' + + const socialLinks = [ + { + key: 'website', + icon: ( + + ), + 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' }, + { key: 'github', network: 'github', title: 'GitHub' }, + { + key: 'swell', + icon: ( + + ), + title: 'Swell' + } + ] + + return ( +
+ {social && ( +
+ {socialLinks.map(({ key, icon, network, title }) => { + const link = social[key as keyof types.AppSocials] + if (!link) return null + + return ( +
+ + + {icon || ( + + )} + + +
+ ) + })} +
+ )} + {!social &&
} + {!isMd && chainName && appName ? ( + + ) : null} +
+ ) +} + +export default SocialButtons diff --git a/src/components/ecosystem/SubcategoryList.tsx b/src/components/ecosystem/SubcategoryList.tsx new file mode 100644 index 00000000..52f3b608 --- /dev/null +++ b/src/components/ecosystem/SubcategoryList.tsx @@ -0,0 +1,73 @@ +/** + * @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 { FormControlLabel, Checkbox } from '@mui/material' +import { cls, cmn } from '@skalenetwork/metaport' +import { highlightMatch } from './SearchBar' + +interface Subcategory { + name: string +} + +interface SubcategoryListProps { + category: string + subcategories: Record + checkedItems: string[] + 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/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx new file mode 100644 index 00000000..4defcd36 --- /dev/null +++ b/src/components/ecosystem/TrendingApps.tsx @@ -0,0 +1,88 @@ +/** + * @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' +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 { 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, + newApps +}) => { + const { getTrendingApps, getAppInfoById } = useLikedApps() + const trendingAppIds = getTrendingApps() + + const appCards = trendingAppIds.map((appId) => { + const { chain, app } = getAppInfoById(appId) + return ( + + + + ) + }) + + if (useCarousel) return {appCards} + + return ( + + {trendingAppIds.map((appId) => { + const { chain, app } = getAppInfoById(appId) + const isNew = isNewApp({ chain, app }, newApps) + return ( + + + + + + ) + })} + + ) +} + +export default TrendingApps 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..707df7bf 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' @@ -69,16 +69,35 @@ 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/' -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, legacy: null, regression: null } export const BASE_METADATA_URL = 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' + +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' + +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/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..23e6efe9 --- /dev/null +++ b/src/core/ecosystem/apps.ts @@ -0,0 +1,83 @@ +/** + * @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' +import { getChainAlias } from '../metadata' + +export function getAllApps(chainsMetadata: types.ChainsMetadataMap): types.AppWithChainAndName[] { + const allApps: types.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: types.AppWithChainAndName[]): types.AppWithChainAndName[] { + return apps.sort((a, b) => a.alias.localeCompare(b.alias)) +} + +export function filterAppsByCategory( + apps: types.AppWithChainAndName[], + checkedItems: string[] +): types.AppWithChainAndName[] { + 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 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 checkedItems.includes(subcategoryKey) + }) + } + return false + }) + }) +} + +export function filterAppsBySearchTerm( + apps: types.AppWithChainAndName[], + searchTerm: string, + chainsMeta: types.ChainsMetadataMap +): types.AppWithChainAndName[] { + if (!searchTerm || searchTerm === '') return apps + const st = searchTerm.toLowerCase() + return apps.filter( + (app) => + 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 new file mode 100644 index 00000000..d8723535 --- /dev/null +++ b/src/core/ecosystem/categories.ts @@ -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 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 = { + ai: { name: 'AI', subcategories: {} }, + bridges: { name: 'Bridges', subcategories: {} }, + dao: { name: 'DAO', subcategories: {} }, + 'data-information': { name: 'Data/Information', subcategories: {} }, + defi: { + name: 'DeFi', + subcategories: { + custody: { name: 'Custody' }, + dex: { name: 'DEX' }, + 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' }, + console: { name: 'Console' }, + fighting: { name: 'Fighting' }, + metaverse: { name: 'Metaverse' }, + mobile: { name: 'Mobile' }, + mmorpg: { name: 'MMORPG' }, + pc: { name: 'PC' }, + 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' }, + web3: { name: 'Web3' } + } + }, + hub: { name: 'Hub', subcategories: {} }, + infrastructure: { name: 'Infrastructure', 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: {} }, + metaverse: { name: 'Metaverse', subcategories: {} }, + web3: { name: 'Web3', subcategories: {} } +} diff --git a/src/core/ecosystem/urlParamsUtil.ts b/src/core/ecosystem/urlParamsUtil.ts new file mode 100644 index 00000000..0c4dba5e --- /dev/null +++ b/src/core/ecosystem/urlParamsUtil.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 urlParamsUtil.ts + * @copyright SKALE Labs 2024-Present + */ + +import { useCallback } from 'react' +import { useSearchParams } from 'react-router-dom' + +export const useUrlParams = () => { + const [searchParams, setSearchParams] = useSearchParams() + + const getCheckedItemsFromUrl = useCallback(() => { + const categories = searchParams.get('categories') + return categories ? categories.split(',') : [] + }, [searchParams]) + + const setCheckedItemsInUrl = useCallback( + (checkedItems: string[]) => { + if (checkedItems.length > 0) { + searchParams.set('categories', checkedItems.join(',')) + } else { + searchParams.delete('categories') + } + setSearchParams(searchParams) + }, + [searchParams, setSearchParams] + ) + + 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/core/ecosystem/utils.ts b/src/core/ecosystem/utils.ts new file mode 100644 index 00000000..5a16bea6 --- /dev/null +++ b/src/core/ecosystem/utils.ts @@ -0,0 +1,91 @@ +/** + * @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 utils.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { type types } from '@/core' +import { categories } from './categories' + +export interface ExpandedItems { + [key: string]: boolean +} + +export const getSelectedSubcategoriesCount = (category: string, checkedItems: string[]): number => { + const subcategories = categories[category].subcategories + if (typeof subcategories === 'object' && !Array.isArray(subcategories)) { + return checkedItems.filter((item) => item.startsWith(`${category}_`)).length + } + return 0 +} + +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) => { + 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 + }) +} + +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/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..b629c199 100644 --- a/src/core/paymaster.ts +++ b/src/core/paymaster.ts @@ -16,12 +16,13 @@ * along with this program. If not, see . */ /** - * @file constants.ts + * @file paymaster.ts * @copyright SKALE Labs 2022-Present */ 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/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/data/contractsMeta.ts b/src/data/contractsMeta.ts index debc9ab0..a031c8cd 100644 --- a/src/data/contractsMeta.ts +++ b/src/data/contractsMeta.ts @@ -17,22 +17,16 @@ */ /** - * @file constants.ts + * @file contractsMeta.ts * @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 }, - staging: { - auto: false, - manager: '', - allocator: '', - grants: null - }, legacy: { auto: false, manager: '0x27C393Cd6CBD071E5F5F2227a915d3fF3650aeaE', diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 450ce94d..92b2a29c 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -21,34 +21,25 @@ * @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' -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' -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 FavoriteBorderOutlinedIcon from '@mui/icons-material/FavoriteBorderOutlined' -import ChainCategories from '../components/ChainCategories' import ChainLogo from '../components/ChainLogo' import SkStack from '../components/SkStack' import Tile from '../components/Tile' @@ -60,35 +51,78 @@ 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' -import { DAPP_RADAR_BASE_URL, MAINNET_CHAIN_LOGOS } 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/CategoriesChips' +import { useLikedApps } from '../LikedAppsContext' +import { useAuth } from '../AuthContext' +import ErrorTile from '../components/ErrorTile' +import { ChipNew, ChipPreTge, ChipTrending } from '../components/Chip' +import { getRecentApps, isNewApp } from '../core/ecosystem/utils' 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() + 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 const [expanded, setExpanded] = useState('panel3') - const [counters, setCounters] = useState(null) + const [counters, setCounters] = useState(null) chain = findChainName(props.chainsMeta, chain ?? '') + const chainMeta = props.chainsMeta[chain] + if (!chainMeta) + return ( + + + + ) - 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] + + if (!appMeta) + return ( + + + + ) + 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 likesCount = appLikes[appId] || 0 + + const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + const isNew = isNewApp({ chain, app }, newApps) + + 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 useEffect(() => { props.loadData() @@ -125,84 +159,76 @@ export default function App(props: { - - -
- , - url: '/chains' - }, - { - text: props.isXs ? 'Hub' : chainAlias, - icon: , - url: `/chains/${chain}` - }, - { - text: appAlias, - icon: - } - ]} - /> -
+ +
+ , + url: '/ecosystem' + }, + { + text: appAlias, + icon: + } + ]} + /> +
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+

{appAlias}

+
+ {trendingAppIds.includes(appId) && } + {isNew && } + {appMeta.tags?.includes('pretge') && } +
+
+ + + +
-
- - -
- - -
+ + - -

{appAlias}

- -
- } - /> - - - - {appMeta.url ? ( - - - - ) : null} - {appMeta.dappradar === undefined || appMeta.dappradar !== false ? ( - - - - ) : null} -
- } - /> {appMeta.contracts ? ( + ) : undefined } tooltip={ @@ -229,50 +255,57 @@ 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/Apps.tsx b/src/pages/Apps.tsx deleted file mode 100644 index 601cfa24..00000000 --- a/src/pages/Apps.tsx +++ /dev/null @@ -1,89 +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 Apps.tsx - * @copyright SKALE Labs 2024-Present - */ - -import { type ReactElement } 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 AppCard from '../components/AppCard' - -import { cmn, cls, type MetaportCore, type interfaces } from '@skalenetwork/metaport' -import { META_TAGS } from '../core/meta' - -export default function Apps(props: { - mpc: MetaportCore - chainsMeta: interfaces.ChainsMetadataMap -}) { - const appCards: ReactElement[] = [] - - for (const schainName in props.chainsMeta) { - if (props.chainsMeta.hasOwnProperty(schainName)) { - const schain = props.chainsMeta[schainName] - if (schain.apps) { - for (const appName in schain.apps) { - if (schain.apps.hasOwnProperty(appName)) { - const card = ( - - - - ) - appCards.push(card) - } - } - } - } - } - - return ( - - - {META_TAGS.apps.title} - - - - - -
-

Ecosystem

-
-

- Explore and interact with apps and games on SKALE Network -

- - - {appCards} - - -
-
- ) -} diff --git a/src/pages/Bridge.tsx b/src/pages/Bridge.tsx index 71a8212f..53c21625 100644 --- a/src/pages/Bridge.tsx +++ b/src/pages/Bridge.tsx @@ -26,9 +26,6 @@ import { Helmet } from 'react-helmet' import { useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' -import Container from '@mui/material/Container' -import Stack from '@mui/material/Stack' - import { CHAINS_META, cls, @@ -39,6 +36,10 @@ import { type interfaces, TransactionData } from '@skalenetwork/metaport' +import { type types } from '@/core' + +import Container from '@mui/material/Container' +import Stack from '@mui/material/Stack' import BridgeBody from '../components/BridgeBody' @@ -54,7 +55,7 @@ function getEmptyTokenParams(): TokenParams { return { keyname: null, type: null } } -export default function Bridge(props: { isXs: boolean }) { +export default function Bridge(props: { isXs: boolean; chainsMeta: types.ChainsMetadataMap }) { const [searchParams, setSearchParams] = useSearchParams() const [tokenParams, setTokenParams] = useState(getEmptyTokenParams()) @@ -186,7 +187,12 @@ export default function Bridge(props: { isXs: boolean }) { ) : null}
- + ) } 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..656030c2 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -24,38 +24,50 @@ import { Helmet } from 'react-helmet' import { useState, useEffect } from 'react' + +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 { cmn, cls, styles, type MetaportCore, type interfaces } from '@skalenetwork/metaport' +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 { Button } from '@mui/material' -import AppChains from '../components/AppChains' -import { IMetrics, ISChain } from '../core/types' +import { MAINNET_CHAIN_NAME } from '../core/constants' 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() + 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 (
@@ -81,77 +93,48 @@ export default function Chains(props: {
-

Chains

+

SKALE Chains

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

-
- - props.chainsMeta[schain.name] && - getPrimaryCategory(props.chainsMeta[schain.name].category) === 'Hub' - )} + + + 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={network} + size="lg" + icon={} + /> + {appChains.length !== 0 && ( + - } + /> + )} + {network !== MAINNET_CHAIN_NAME && otherChains.length !== 0 && ( + - (props.chainsMeta[schain.name] && - getPrimaryCategory(props.chainsMeta[schain.name].category) === 'AppChain') || - !props.chainsMeta[schain.name] - )} - isXs={props.isXs} + metrics={props.metrics} + skaleNetwork={network} + size="md" + icon={} /> -
-
+ )} ) diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx new file mode 100644 index 00000000..7d124e49 --- /dev/null +++ b/src/pages/Ecosystem.tsx @@ -0,0 +1,223 @@ +/** + * @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 { 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 { Tab, Tabs } from '@mui/material' + +import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded' +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 } from '@skalenetwork/metaport' +import { META_TAGS } from '../core/meta' +import CategoryDisplay from '../components/ecosystem/Categories' +import { + filterAppsByCategory, + filterAppsBySearchTerm, + getAllApps, + sortAppsByAlias +} from '../core/ecosystem/apps' + +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' +import { MAX_APPS_DEFAULT, SKALE_SOCIAL_LINKS } from '../core/constants' +import SocialButtons from '../components/ecosystem/Socials' + +export default function Ecosystem(props: { + mpc: MetaportCore + chainsMeta: types.ChainsMetadataMap + isXs: boolean +}) { + const { getCheckedItemsFromUrl, setCheckedItemsInUrl, getTabIndexFromUrl, setTabIndexInUrl } = + useUrlParams() + const allApps = useMemo(() => sortAppsByAlias(getAllApps(props.chainsMeta)), [props.chainsMeta]) + const [checkedItems, setCheckedItems] = useState([]) + const [filteredApps, setFilteredApps] = useState([]) + const [searchTerm, setSearchTerm] = useState('') + const [activeTab, setActiveTab] = useState(0) + + const newApps = useMemo( + () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), + [props.chainsMeta] + ) + + useEffect(() => { + const initialCheckedItems = getCheckedItemsFromUrl() + setCheckedItems(initialCheckedItems) + const initialTabIndex = getTabIndexFromUrl() + setActiveTab(initialTabIndex) + }, []) + + useEffect(() => { + const filtered = filterAppsBySearchTerm( + filterAppsByCategory(allApps, checkedItems), + searchTerm, + props.chainsMeta + ) + setFilteredApps(filtered) + }, [allApps, checkedItems, searchTerm]) + + const handleSetCheckedItems = (newCheckedItems: string[]) => { + setCheckedItems(newCheckedItems) + setCheckedItemsInUrl(newCheckedItems) + } + + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue) + setTabIndexInUrl(newValue) + } + + const filteredNewApps = useMemo(() => { + return newApps.filter((app) => + filteredApps.some( + (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.app + ) + ) + }, [newApps, filteredApps]) + + return ( + + + {META_TAGS.apps.title} + + + + + +
+
+

Ecosystem

+

+ Explore dApps across the SKALE ecosystem +

+
+
+ +
+
+ + + + + + + + } + 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')} + /> + + + {activeTab === 0 && ( + + )} + {activeTab === 1 && ( + + )} + {activeTab === 2 && ( + + )} + {activeTab === 3 && ( + + )} + +
+
+ ) +} 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 ( 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 ? ( Promise - chainsMeta: interfaces.ChainsMetadataMap + chainsMeta: types.ChainsMetadataMap }) { const [_, setIntervalId] = useState() + const newApps = useMemo( + () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), + [props.chainsMeta] + ) useEffect(() => { props.loadData() @@ -54,46 +72,15 @@ export default function Start(props: { setIntervalId(intervalId) }, []) - let appCards: any = [] - - function isLegacyApp(chain: string, app: string): boolean { - if (props.chainsMeta[chain].apps === undefined) return false - if (!props.chainsMeta[chain].apps![app]) return false - return !!props.chainsMeta[chain].apps![app].legacy - } - - const apps = props.topApps - ? props.topApps.filter((topApp) => !isLegacyApp(topApp.chain, topApp.app)) - : null - - if (apps) { - appCards = apps.slice(0, 4).map((topApp: IAppId) => ( - - - - )) - } - return ( -

🔥 Top Apps on SKALE

- - {appCards} - -

- ⭐ Featured Apps -

- -

- 🪐 Explore Portal -

+

Welcome to SKALE

+ } + className={cls(cmn.mbott10, cmn.mtop20)} + /> @@ -105,28 +92,78 @@ export default function Start(props: { } + description="Manage delegations and validators" + name="stake" + url="/staking" + icon={} /> } + description="Chains info, block explorers and endpoints" + name="SKALE Chains" + url="/chains" + icon={} /> } + description="Discover apps and games on SKALE" + name="ecosystem" + icon={} /> +
+ } /> + + + +
+ +
+ } + /> + + + +
+ +
+ } + /> + + + +
+
+ } + className={cls(cmn.mbott10, cmn.mtop20, cmn.ptop20)} + /> +
) } diff --git a/src/styles/chip.scss b/src/styles/chip.scss new file mode 100644 index 00000000..21433da6 --- /dev/null +++ b/src/styles/chip.scss @@ -0,0 +1,21 @@ +.chipContainer { + display: flex; + overflow-x: auto; + white-space: nowrap; + gap: 8px; + border-radius: 25px; + position: relative; +} + +.chipContainer::-webkit-scrollbar { + display: none; +} + +.chipContainer { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.chipContainer>* { + flex: 0 0 auto; +} \ No newline at end of file diff --git a/src/styles/components.scss b/src/styles/components.scss new file mode 100644 index 00000000..6ebc2573 --- /dev/null +++ b/src/styles/components.scss @@ -0,0 +1,141 @@ +.skTabs { + .MuiTab-root, .MuiButton-root { + background-color: $sk-bg !important; + } +} + +.responsive-app-header { + display: flex; + align-items: flex-start; +} + +.sk-app-logo { + flex-shrink: 0; +} + +.logo-wrapper { + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + overflow: hidden; +} + +.responsive-logo { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.app-info { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 20px; +} + +@media (max-width: 600px) { + .responsive-app-header { + flex-direction: column; + } + + .app-info { + margin-left: 0; + margin-top: 20px; + } +} + +.sk-logo-sm { + width: 80px; + height: 80px; +} + +.sk-logo-md { + width: 170px; + height: 170px; + .responsive-logo { + padding: 20px !important; + } +} + +.sk-app-logo { + .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 + } + +} + +.sk-app-card { + padding: 16px; + + .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 diff --git a/vercel.json b/vercel.json index e6fa932e..ade62784 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' https://chain-proxy.wallet.coinbase.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' https://chain-proxy.wallet.coinbase.com 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",