diff --git a/.gitignore b/.gitignore index a589be2f..f82eb5a3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ src/data/metaportConfig*.ts src/meta/ src/assets/validators/index.ts src/metadata.json +src/metrics.json src/metadata/chainsData.json src/metadata/faucet.json diff --git a/bun.lockb b/bun.lockb index 7ae63c3a..582bbccc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 6e89b1a6..14fac057 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "portal", "private": true, - "version": "3.0.1", + "version": "3.1.0", "type": "module", "scripts": { "build:testnet": "NETWORK_NAME=testnet bash build.sh", "build:mainnet": "NETWORK_NAME=mainnet bash build.sh", + "build:qa": "NETWORK_NAME=legacy bash build.sh", "build:portal": "tsc && vite build", "build:packages": "bun run build:core", "build:core": "cd packages/core && bun install && bun run build", @@ -20,11 +21,12 @@ "@mdx-js/rollup": "^2.3.0", "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", - "@skalenetwork/metaport": "3.0.0-develop.5", + "@skalenetwork/metaport": "3.1.0-develop.0", "@skalenetwork/skale-contracts-ethers-v6": "1.0.1", "@transak/transak-sdk": "^3.1.1", "@types/react-copy-to-clipboard": "^5.0.4", "@vercel/analytics": "^1.0.2", + "embla-carousel-react": "^8.3.0", "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "^39.1.0", "prettier": "^3.0.3", diff --git a/packages/core/src/types/ChainsMetadata.ts b/packages/core/src/types/ChainsMetadata.ts index ffc11813..c25d3888 100644 --- a/packages/core/src/types/ChainsMetadata.ts +++ b/packages/core/src/types/ChainsMetadata.ts @@ -42,10 +42,16 @@ export interface AppMetadata { description?: string contracts?: string[] social?: AppSocials - tags?: string[] added?: number categories: CategoriesMap + pretge?: TimeRange } + +interface TimeRange { + from: number + to: number +} + export interface AppWithChainAndName extends AppMetadata { chain: string appName: string diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 791bf404..805d2b81 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -135,6 +135,9 @@ export interface IAddressCounters { token_transfers_count: string transactions_count: string validations_count: string + transactions_today: number + transactions_last_7_days: number + transactions_last_30_days: number } export interface IStats { diff --git a/sitemap_template.xml b/sitemap_template.xml index f3da855a..d758e5eb 100644 --- a/sitemap_template.xml +++ b/sitemap_template.xml @@ -10,6 +10,11 @@ {BASE_URL}/chains + + + {BASE_URL}/ecosystem + + {BASE_URL}/bridge/history diff --git a/skale-network b/skale-network index d41f6fb3..2e53298d 160000 --- a/skale-network +++ b/skale-network @@ -1 +1 @@ -Subproject commit d41f6fb3728adb318bdd869e3a2f87672cf10f2c +Subproject commit 2e53298d4a4393c4b7a1a77a3b1cd7bae750e374 diff --git a/src/App.scss b/src/App.scss index 0d5b8e41..cf132899 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,6 +1,7 @@ @import './variables'; @import './styles/components'; @import './styles/chip'; +@import './styles/ProfileModal'; :root { background: black; @@ -24,6 +25,10 @@ body { width: 75pt; } +.skMessage { + min-height: 58px; +} + .sk-header { background-color: rgba(0, 0, 0, 0) !important; -webkit-backdrop-filter: blur(80px) !important; @@ -389,6 +394,38 @@ body::-webkit-scrollbar { } +.sk-icon-btn-small { + width: 34px; + height: 34px; + + svg { + width: 18px; + height: 18px; + } +} + +.sk-icon-btn-medium { + width: 40px; + height: 40px; + + svg { + width: 24px; + height: 24px; + } +} + + +.sk-icon-btn-large { + width: 46px; + height: 46px; + + svg { + width: 32px; + height: 32px; + } +} + + .btn { text-transform: none !important; font-size: 0.8025rem !important; @@ -762,6 +799,15 @@ input[type=number] { } } +.chipMostLiked { + background: linear-gradient(180deg, #e8a25b, #e58e36) !important; + + + p { + color: black !important + } +} + .chipNewApp { background: linear-gradient(180deg, #65a974, #508d5e) !important; @@ -800,7 +846,7 @@ input[type=number] { .chipXs { border-radius: 15px; - padding: 4px 8px; + padding: 4px 6px; svg { width: 12px; @@ -1138,6 +1184,11 @@ input[type=number] { padding-bottom: 5px; } +.m-ri-min10 { + margin-right: -10px; +} + + .validatorIcon { border-radius: 50%; width: 70px; @@ -1183,7 +1234,7 @@ input[type=number] { .amountInput { input { - padding: 3px 10px 0 5px !important + padding: 0 !important } } @@ -1373,4 +1424,9 @@ input[type=number] { .hidden { display: none; +} + +.MuiTooltip-tooltip { + font-size: 0.8rem !important; + padding: 8px 12px !important; } \ No newline at end of file diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx index d0822f17..1dfe775f 100644 --- a/src/AuthContext.tsx +++ b/src/AuthContext.tsx @@ -20,7 +20,7 @@ * @copyright SKALE Labs 2024-Present */ -import React, { createContext, useState, useContext, useEffect } from 'react' +import React, { createContext, useState, useContext, useEffect, useCallback, useMemo } from 'react' import { Logger, type ILogObj } from 'tslog' import { SiweMessage } from 'siwe' import { useWagmiAccount } from '@skalenetwork/metaport' @@ -30,23 +30,38 @@ const log = new Logger({ name: 'AuthContext' }) interface AuthContextType { isSignedIn: boolean + email: string | null + isEmailLoading: boolean + isEmailUpdating: boolean + emailError: string | null + isProfileModalOpen: boolean handleSignIn: () => Promise handleSignOut: () => Promise handleAddressChange: () => Promise getSignInStatus: () => Promise + getEmail: () => Promise + updateEmail: (newEmail: string) => Promise + openProfileModal: () => void + closeProfileModal: () => void } const AuthContext = createContext(undefined) export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isSignedIn, setIsSignedIn] = useState(false) + const [email, setEmail] = useState(null) + const [isEmailLoading, setIsEmailLoading] = useState(false) + const [isEmailUpdating, setIsEmailUpdating] = useState(false) + const [emailError, setEmailError] = useState(null) + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false) const { address } = useWagmiAccount() - const handleAddressChange = async function () { + const handleAddressChange = useCallback(async () => { log.info(`Address changed: ${address}`) if (!address) { log.warn('No address found, signing out') setIsSignedIn(false) + setEmail(null) return } try { @@ -54,6 +69,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (status) { log.info('User is already signed in') setIsSignedIn(true) + await getEmail() } else { log.info('User not signed in, clearing session') await handleSignOut() @@ -62,9 +78,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children log.error('Error checking sign-in status:', error) setIsSignedIn(false) } - } + }, [address]) - const getSignInStatus = async (): Promise => { + const getSignInStatus = useCallback(async (): Promise => { try { if (!address) return false log.info(`Checking sign-in status for address: ${address}`) @@ -80,9 +96,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children log.error('Error checking sign-in status:', error) return false } - } + }, [address]) - const handleSignIn = async () => { + const handleSignIn = useCallback(async () => { if (!address) { log.warn('Cannot sign in: No address provided') return @@ -116,15 +132,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (response.ok) { log.info('Sign in successful') setIsSignedIn(true) + await getEmail() } else { log.error('Sign in failed', response.status, response.statusText) } } catch (error) { log.error('Error signing in:', error) } - } + }, [address]) - const handleSignOut = async () => { + const handleSignOut = useCallback(async () => { log.info('Initiating sign out') try { const response = await fetch(`${API_URL}/auth/signout`, { @@ -140,29 +157,116 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children log.error('Error signing out:', error) } finally { setIsSignedIn(false) + setEmail(null) } - } + }, []) - const fetchNonce = async () => { + const fetchNonce = useCallback(async () => { log.debug('Fetching nonce') const response = await fetch(`${API_URL}/auth/nonce`) const data = await response.json() log.debug(`Nonce received: ${data.nonce}`) return data.nonce - } + }, []) + + const getEmail = useCallback(async () => { + setIsEmailLoading(true) + setEmailError(null) + try { + const response = await fetch(`${API_URL}/auth/email`, { + credentials: 'include' + }) + if (response.ok) { + if (response.status === 204) { + setEmail(null) + } else { + const data = await response.json() + setEmail(data.email) + } + } else { + throw new Error('Failed to fetch email') + } + } catch (error) { + console.error('Error fetching email:', error) + setEmailError('Failed to fetch email') + } finally { + setIsEmailLoading(false) + } + }, []) + + const updateEmail = useCallback(async (newEmail: string) => { + setIsEmailUpdating(true) + setEmailError(null) + try { + const response = await fetch(`${API_URL}/auth/update_email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: newEmail }), + credentials: 'include' + }) + if (response.ok) { + setEmail(newEmail) + } else { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to update email') + } + } catch (error: any) { + log.error('Error updating email:', error) + setEmailError(error.message || 'Failed to update email') + } finally { + setIsEmailUpdating(false) + } + }, []) + + const openProfileModal = useCallback(() => { + setIsProfileModalOpen(true) + }, []) + + const closeProfileModal = useCallback(() => { + setIsProfileModalOpen(false) + }, []) useEffect(() => { log.info('Address changed, updating auth status') handleAddressChange() - }, [address]) + }, [address, handleAddressChange]) - return ( - - {children} - + const contextValue = useMemo( + () => ({ + isSignedIn, + email, + isEmailLoading, + isEmailUpdating, + emailError, + isProfileModalOpen, + handleSignIn, + handleSignOut, + handleAddressChange, + getSignInStatus, + getEmail, + updateEmail, + openProfileModal, + closeProfileModal + }), + [ + isSignedIn, + email, + isEmailLoading, + isEmailUpdating, + emailError, + isProfileModalOpen, + handleSignIn, + handleSignOut, + handleAddressChange, + getSignInStatus, + getEmail, + updateEmail, + openProfileModal, + closeProfileModal + ] ) + + return {children} } export const useAuth = () => { @@ -172,4 +276,4 @@ export const useAuth = () => { throw new Error('useAuth must be used within an AuthProvider') } return context -} \ No newline at end of file +} diff --git a/src/Header.tsx b/src/Header.tsx index 94e4654e..9aeb9e84 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -33,6 +33,7 @@ import HelpZen from './components/HelpZen' import MoreMenu from './components/MoreMenu' import AccountMenu from './components/AccountMenu' import NetworkSwitch from './components/NetworkSwitch' +import GetSFuel from './components/GetSFuel' import { MAINNET_CHAIN_NAME } from './core/constants' import { Link } from 'react-router-dom' @@ -62,6 +63,7 @@ export default function Header(props: { address: `0x${string}` | undefined; mpc: ) : null} + diff --git a/src/LikedAppsContext.tsx b/src/LikedAppsContext.tsx index 926f7192..fc8596fd 100644 --- a/src/LikedAppsContext.tsx +++ b/src/LikedAppsContext.tsx @@ -38,8 +38,8 @@ interface LikedAppsContextType { refreshLikedApps: () => Promise getAppId: (chainName: string, appName: string) => string getAppInfoById: (appId: string) => types.IAppId - getTrendingApps: () => string[] - getTrendingRank: (trendingAppIds: string[], appId: string) => number | undefined + getMostLikedApps: () => string[] + getMostLikedRank: (mostLikedAppIds: string[], appId: string) => number | undefined } const LikedAppsContext = createContext(undefined) @@ -133,15 +133,15 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi return { chain, app } } - const getTrendingApps = useCallback(() => { + const getMostLikedApps = useCallback(() => { return Object.entries(appLikes) .sort(([, likesA], [, likesB]) => likesB - likesA) .slice(0, MAX_APPS_DEFAULT) .map(([appId]) => appId) }, [appLikes]) - const getTrendingRank = (trendingAppIds: string[], appId: string): number | undefined => { - const idx = trendingAppIds.indexOf(appId) + const getMostLikedRank = (mostLikedAppIds: string[], appId: string): number | undefined => { + const idx = mostLikedAppIds.indexOf(appId) return idx === -1 ? undefined : idx + 1 } @@ -156,8 +156,8 @@ export const LikedAppsProvider: React.FC<{ children: React.ReactNode }> = ({ chi refreshLikedApps: fetchLikedApps, getAppId, getAppInfoById, - getTrendingApps, - getTrendingRank + getMostLikedApps, + getMostLikedRank }} > {children} diff --git a/src/Portal.tsx b/src/Portal.tsx index e68362a4..9b109dd7 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -29,6 +29,7 @@ import Header from './Header' import SkDrawer from './SkDrawer' import Router from './Router' import SkBottomNavigation from './SkBottomNavigation' +import ProfileModal from './components/profile/ProfileModal' export default function Portal() { const mpc = useMetaportStore((state) => state.mpc) @@ -41,6 +42,7 @@ export default function Portal() {
+
diff --git a/src/Router.tsx b/src/Router.tsx index 4719c804..61d0a8e8 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -257,7 +257,14 @@ export default function Router() { } + element={ + + } /> } /> @@ -299,7 +306,15 @@ export default function Router() { /> } + element={ + + } /> + + + + + + + + + + -

Bridge

+

Transfer

- + @@ -87,19 +100,6 @@ export default function SkDrawer() { - - - - - - - - - -

Network

@@ -173,17 +173,6 @@ export default function SkDrawer() { - - - - - - - - - - - diff --git a/src/assets/discord-mark-white.svg b/src/assets/discord-mark-white.svg deleted file mode 100644 index 7f9a31f0..00000000 --- a/src/assets/discord-mark-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/validators/v4.png b/src/assets/validators/v4.png new file mode 100644 index 00000000..871fb82e Binary files /dev/null and b/src/assets/validators/v4.png differ diff --git a/src/assets/validators/v4.webp b/src/assets/validators/v4.webp deleted file mode 100644 index 672dcd76..00000000 Binary files a/src/assets/validators/v4.webp and /dev/null differ diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx index 21bb3848..8353c1a6 100644 --- a/src/components/AccountMenu.tsx +++ b/src/components/AccountMenu.tsx @@ -21,21 +21,12 @@ * @copyright SKALE Labs 2023-Present */ -import { useState, type MouseEvent } from 'react' import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' import Box from '@mui/material/Box' -import Menu from '@mui/material/Menu' -import MenuItem from '@mui/material/MenuItem' import Tooltip from '@mui/material/Tooltip' import Button from '@mui/material/Button' - -import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' -import SignalCellularAltOutlinedIcon from '@mui/icons-material/SignalCellularAltOutlined' -import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' import LooksRoundedIcon from '@mui/icons-material/LooksRounded' -import LoginOutlinedIcon from '@mui/icons-material/LoginOutlined' -import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined' import FiberManualRecordRoundedIcon from '@mui/icons-material/FiberManualRecordRounded' import { cls, styles, cmn, RainbowConnectButton } from '@skalenetwork/metaport' @@ -43,163 +34,63 @@ import { cls, styles, cmn, RainbowConnectButton } from '@skalenetwork/metaport' import { useAuth } from '../AuthContext' export default function AccountMenu(props: any) { - const { isSignedIn, handleSignIn, handleSignOut } = useAuth() - const [anchorEl, setAnchorEl] = useState(null) - const open = Boolean(anchorEl) - const handleClick = (event: MouseEvent) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - + const { isSignedIn, openProfileModal } = useAuth() return ( -
- - {!props.address ? ( - -
- - {({ openConnectModal }) => { - return ( - - ) - }} - -
-
- ) : ( - + {!props.address ? ( + +
+ + {({ openConnectModal }) => { + return ( + + ) + }} + +
+
+ ) : ( + + - - )} -
- - - {({ openAccountModal }) => { - return ( - { - openAccountModal() - handleClose() - }} - > - Account info - - ) - }} - - - -
-
-
View on Etherscan
-
- -
-
-
- { - if (isSignedIn) { - handleSignOut() - } else { - handleSignIn() - } - handleClose() - }} - > -
- {isSignedIn ? ( - - ) : ( - - )} -
-
Sign {isSignedIn ? 'out' : 'in'}
-
-
-
+ {props.address.substring(0, 5) + + '...' + + props.address.substring(props.address.length - 3)} + + + )} + ) } diff --git a/src/components/Carousel.tsx b/src/components/Carousel.tsx index eb01cd02..6232c41a 100644 --- a/src/components/Carousel.tsx +++ b/src/components/Carousel.tsx @@ -32,9 +32,10 @@ import { cmn, cls } from '@skalenetwork/metaport' interface CarouselProps { children: ReactNode[] showArrows?: boolean + className?: string } -const Carousel: React.FC = ({ children, showArrows = true }) => { +const Carousel: React.FC = ({ children, showArrows = true, className }) => { const [startIndex, setStartIndex] = useState(0) const theme = useTheme() const isXs = useMediaQuery(theme.breakpoints.only('xs')) @@ -61,7 +62,7 @@ const Carousel: React.FC = ({ children, showArrows = true }) => { const visibleChildren = children.slice(startIndex, startIndex + itemsToShow) return ( - + = ({ children, showArrows = true }) => { onClick={handlePrev} disabled={startIndex === 0} size="small" - className={cls('outlined', cmn.mri5)} + className={cls('filled', cmn.mri5)} > @@ -100,7 +101,7 @@ const Carousel: React.FC = ({ children, showArrows = true }) => { onClick={handleNext} disabled={startIndex >= children.length - itemsToShow} size="small" - className={cls(cmn.pSec, 'outlined', cmn.mleft5)} + className={cls(cmn.pSec, 'filled', cmn.mleft5)} > diff --git a/src/components/Chip.tsx b/src/components/Chip.tsx index a613aad6..1c7fa3ce 100644 --- a/src/components/Chip.tsx +++ b/src/components/Chip.tsx @@ -53,6 +53,10 @@ export const ChipTrending: React.FC<{ trending?: number }> = ({ trending }) => { ) } +export const ChipMostLiked: React.FC<{}> = ({}) => { + return +} + export const ChipNew: React.FC<{}> = ({}) => { return } diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 15c177d5..72eccc19 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -49,7 +49,7 @@ export default function ConnectWallet(props: {
-

+

{props.customText ?? 'Connect your wallet to continue'}

diff --git a/src/components/GetSFuel.tsx b/src/components/GetSFuel.tsx new file mode 100644 index 00000000..5364d52b --- /dev/null +++ b/src/components/GetSFuel.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 GetSFuel.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { Box, Button, Tooltip } from '@mui/material' +import BoltRoundedIcon from '@mui/icons-material/BoltRounded' +import AutoModeRoundedIcon from '@mui/icons-material/AutoModeRounded' +import { cls, styles, cmn, type MetaportCore, useWagmiAccount } from '@skalenetwork/metaport' +import { usesFuel } from '../useSFuel' + +export default function GetSFuel({ mpc }: { mpc: MetaportCore }) { + const { sFuelOk, isMining, mineSFuel, sFuelCompletionPercentage, loading } = usesFuel(mpc) + const { address } = useWagmiAccount() + if (!address) return null + + function btnText() { + if (isMining) return `Getting sFUEL - ${sFuelCompletionPercentage}%` + if (loading) return 'Checking sFUEL' + return sFuelOk ? 'sFUEL OK' : 'Get sFUEL' + } + + return ( + + + + + + ) +} diff --git a/src/components/HelpZen.tsx b/src/components/HelpZen.tsx index a094ffc2..b34c708f 100644 --- a/src/components/HelpZen.tsx +++ b/src/components/HelpZen.tsx @@ -2,14 +2,13 @@ import { Link } from 'react-router-dom' import { useEffect, useState, type MouseEvent } from 'react' import Box from '@mui/material/Box' -import Tooltip from '@mui/material/Tooltip' -import IconButton from '@mui/material/IconButton' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' import QuestionMarkRoundedIcon from '@mui/icons-material/QuestionMarkRounded' import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' import MarkUnreadChatAltRoundedIcon from '@mui/icons-material/MarkUnreadChatAltRounded' -import { cls, styles, cmn } from '@skalenetwork/metaport' +import { cmn } from '@skalenetwork/metaport' +import SkIconBtn from './SkIconBth' export default function HelpZen() { const [anchorEl, setAnchorEl] = useState(null) @@ -44,22 +43,12 @@ export default function HelpZen() { className={cmn.mleft5} sx={{ display: 'flex', alignItems: 'center', textAlign: 'center' }} > - - - - - + , new: , trending: , + mostLiked: , categories: } diff --git a/src/components/Message.tsx b/src/components/Message.tsx index 18209ac2..2054af8e 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -26,8 +26,12 @@ import IconButton from '@mui/material/IconButton' import CloseRoundedIcon from '@mui/icons-material/CloseRounded' import Collapse from '@mui/material/Collapse' import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded' -import { SkPaper, cls, cmn } from '@skalenetwork/metaport' +import { SkPaper, cls, cmn, useWagmiAccount } from '@skalenetwork/metaport' import { Link } from 'react-router-dom' +import { Button } from '@mui/material' + +import { useAuth } from '../AuthContext' +import SwellIcon from './ecosystem/SwellIcon' export default function Message(props: { text: string | null @@ -38,25 +42,38 @@ export default function Message(props: { showOnLoad?: boolean | undefined type?: 'warning' | 'info' | 'error' closable?: boolean + button?: ReactElement | null + gray?: boolean }) { const type = props.type ?? 'info' const [show, setShow] = useState(true) const closable = props.closable ?? true + const gray = props.gray ?? true return (
-
{props.icon}
+
{props.icon}
{props.text ? (

{props.text} @@ -75,23 +92,42 @@ export default function Message(props: { ) : null}

+ {props.button} {closable ? ( -
- { - setShow(false) - }} - className={cls(cmn.paperGrey, cmn.mleft10)} - > - - -
+ { + setShow(false) + }} + className={cls(cmn.paperGrey, cmn.mleft10)} + > + + ) : null}
) } + +export function SwellMessage(props: { className?: string }) { + const { openProfileModal, isEmailLoading, email } = useAuth() + const { address } = useWagmiAccount() + + if ((!isEmailLoading && email) || !address) return + return ( + } + text="Complete your profile to receive quest rewards on SKALE Swell" + closable={false} + button={ + + } + /> + ) +} diff --git a/src/components/MetricsWarning.tsx b/src/components/MetricsWarning.tsx index 06eb00c6..bf9c2b3a 100644 --- a/src/components/MetricsWarning.tsx +++ b/src/components/MetricsWarning.tsx @@ -30,11 +30,10 @@ import Message from './Message' import { timestampToDate } from '../core/helper' -const FOUR_HOURS_IN_SECONDS = 4 * 60 * 60 +const MAX_DELAY_SECONDS = 48 * 60 * 60 export default function MetricsWarning(props: { metrics: types.IMetrics | null }) { - if (!props.metrics || Date.now() / 1000 - props.metrics.last_updated < FOUR_HOURS_IN_SECONDS) - return + if (!props.metrics || Date.now() / 1000 - props.metrics.last_updated < MAX_DELAY_SECONDS) return return ( (null) @@ -114,44 +111,12 @@ export default function MoreMenu() { Changelog - - -
- -
-
SKALE Website
-
- -
-
-
- - -
- -
-
Docs portal
-
- -
-
-
- +
- discord logo +
-
Discord
+
SKALE Network Docs
diff --git a/src/components/SkIconBth.tsx b/src/components/SkIconBth.tsx new file mode 100644 index 00000000..8d6429b7 --- /dev/null +++ b/src/components/SkIconBth.tsx @@ -0,0 +1,110 @@ +/** + * @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 SkIconBth.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React, { useState, useCallback } from 'react' +import IconButton, { IconButtonProps } from '@mui/material/IconButton' +import Tooltip, { TooltipProps } from '@mui/material/Tooltip' +import { SvgIconProps } from '@mui/material/SvgIcon' +import { useTheme } from '@mui/material/styles' +import useMediaQuery from '@mui/material/useMediaQuery' +import { cls, styles, cmn } from '@skalenetwork/metaport' + +type IconType = React.ComponentType + +interface SkIconBtnProps extends Omit { + icon: IconType + iconClassName?: string + tooltipTitle?: React.ReactNode + tooltipProps?: Partial + primary?: boolean +} + +const SkIconBtn: React.FC = ({ + icon: Icon, + onClick, + className, + iconClassName, + size = 'medium', + tooltipTitle, + tooltipProps, + primary = true, + ...props +}) => { + const [tooltipOpen, setTooltipOpen] = useState(false) + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + + const handleTooltipToggle = useCallback(() => { + if (isMobile) { + setTooltipOpen((prevOpen) => !prevOpen) + } + }, [isMobile]) + + const handleClick = useCallback( + (event: React.MouseEvent) => { + if (isMobile) { + handleTooltipToggle() + } + if (onClick) { + onClick(event) + } + }, + [isMobile, onClick, handleTooltipToggle] + ) + + const button = ( + + + + ) + + if (tooltipTitle) { + return ( + + {button} + + ) + } + + return button +} + +export default SkIconBtn diff --git a/src/components/SkPageInfoIcon.tsx b/src/components/SkPageInfoIcon.tsx new file mode 100644 index 00000000..e87c076f --- /dev/null +++ b/src/components/SkPageInfoIcon.tsx @@ -0,0 +1,43 @@ +/** + * @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 SkPageInfoIcon.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' +import SkIconBtn from './SkIconBth' + +interface SkPageInfoIconProps { + meta_tag: any +} + +const SkPageInfoIcon: React.FC = ({ meta_tag }) => { + return ( + + ) +} + +export default SkPageInfoIcon diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx index d0db7941..59ffad43 100644 --- a/src/components/Tile.tsx +++ b/src/components/Tile.tsx @@ -44,8 +44,8 @@ export default function Tile(props: { color?: DueDateStatus progressColor?: DueDateStatus progress?: number - children?: ReactElement | ReactElement[] - childrenRi?: ReactElement | ReactElement[] + children?: ReactElement | ReactElement[] | false + childrenRi?: ReactElement | ReactElement[] | null | '' size?: 'lg' | 'md' textColor?: string disabled?: boolean | null @@ -161,6 +161,7 @@ export default function Tile(props: { ) : null} {props.value && !props.copy ? value : null} + {props.children &&
{props.children}
} {!props.value && !props.children ? ( ) : null} @@ -177,7 +178,6 @@ export default function Tile(props: {
{props.childrenRi} - {props.children}
) diff --git a/src/components/chains/HubTile.tsx b/src/components/chains/HubTile.tsx index 407c4517..95f9eda1 100644 --- a/src/components/chains/HubTile.tsx +++ b/src/components/chains/HubTile.tsx @@ -73,12 +73,14 @@ export default function HubTile(props: {
- +
+ +
diff --git a/src/components/delegation/RetrieveRewardModal.tsx b/src/components/delegation/RetrieveRewardModal.tsx index 41e1bc5f..e25e006a 100644 --- a/src/components/delegation/RetrieveRewardModal.tsx +++ b/src/components/delegation/RetrieveRewardModal.tsx @@ -103,7 +103,7 @@ export default function RetrieveRewardModal(props: { -

+

Confirm reward retrieval

} className={cls(cmn.mbott10)} + closable={false} /> + +
+ ) : ( + email && ( + + ) + ) + } + /> + ) +} + +export default EmailSection diff --git a/src/components/profile/ProfileModal.tsx b/src/components/profile/ProfileModal.tsx new file mode 100644 index 00000000..c8e0d273 --- /dev/null +++ b/src/components/profile/ProfileModal.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 ProfileModal.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { Modal, Box, useTheme, useMediaQuery } from '@mui/material' +import { cls, cmn, SkPaper, useWagmiAccount } from '@skalenetwork/metaport' +import { useAuth } from '../../AuthContext' +import Tile from '../Tile' +import ConnectWallet from '../ConnectWallet' +import ProfileModalHeader from './ProfileModalHeader' +import ProfileModalActions from './ProfileModalActions' +import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' + +const ProfileModal: React.FC = () => { + const { address } = useWagmiAccount() + const { isSignedIn, handleSignIn, handleSignOut, isProfileModalOpen, closeProfileModal } = + useAuth() + + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('sm')) + + const modalContent = ( + + + + {!address ? ( + + ) : ( +
+ )} + {address ? ( +
+ } + copy={address} + className={cls(cmn.mbott10)} + /> + +
+ ) : ( +
+ )} +
+
+ ) + + return ( + + {modalContent} + + ) +} + +export default ProfileModal diff --git a/src/components/profile/ProfileModalActions.tsx b/src/components/profile/ProfileModalActions.tsx new file mode 100644 index 00000000..2451ddf0 --- /dev/null +++ b/src/components/profile/ProfileModalActions.tsx @@ -0,0 +1,86 @@ +/** + * @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 ProfileModalActions.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { Button } from '@mui/material' +import { cls, RainbowConnectButton } from '@skalenetwork/metaport' +import SkStack from '../SkStack' +import LaunchIcon from '@mui/icons-material/Launch' +import LooksRoundedIcon from '@mui/icons-material/LooksRounded' +import LoginIcon from '@mui/icons-material/Login' +import LogoutIcon from '@mui/icons-material/Logout' + +interface ProfileModalActionsProps { + address: string + isSignedIn: boolean + isMobile: boolean + handleSignIn: () => void + handleSignOut: () => void + className?: string +} + +const ProfileModalActions: React.FC = ({ + address, + isSignedIn, + isMobile, + handleSignIn, + handleSignOut, + className +}) => ( + + + + + {({ openAccountModal }) => ( + + )} + + + +) + +export default ProfileModalActions diff --git a/src/components/profile/ProfileModalHeader.tsx b/src/components/profile/ProfileModalHeader.tsx new file mode 100644 index 00000000..7a67d80c --- /dev/null +++ b/src/components/profile/ProfileModalHeader.tsx @@ -0,0 +1,53 @@ +/** + * @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 ProfileModalHeader.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { FiberManualRecord } from '@mui/icons-material' +import { cls, cmn } from '@skalenetwork/metaport' +import Headline from '../Headline' +import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' + +interface ProfileModalHeaderProps { + address: string | undefined + isSignedIn: boolean +} + +const ProfileModalHeader: React.FC = ({ address, isSignedIn }) => ( +
+
+ } size="small" /> +
+
+ +

+ {address ? (isSignedIn ? 'Signed in' : 'Connected but not signed in') : 'Not connected'} +

+
+
+) + +export default ProfileModalHeader diff --git a/src/components/profile/SwellMessage.tsx b/src/components/profile/SwellMessage.tsx new file mode 100644 index 00000000..80fce921 --- /dev/null +++ b/src/components/profile/SwellMessage.tsx @@ -0,0 +1,68 @@ +/** + * @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 SwellMessage.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import { Button, Link } from '@mui/material' +import { cls, cmn } from '@skalenetwork/metaport' +import Message from '../Message' +import SwellIcon from '../ecosystem/SwellIcon' +import { SKALE_SOCIAL_LINKS } from '../../core/constants' + +interface SwellMessageProps { + email: string | null + isEditing: boolean + handleStartEditing: () => void +} + +const SwellMessage: React.FC = ({ email, isEditing, handleStartEditing }) => ( + } + text={ + email + ? 'Claim your quest rewards on SKALE Swell' + : 'Add your email to complete profile and claim rewards' + } + closable={false} + button={ + email ? ( + + + + ) : !isEditing ? ( + + ) : null + } + /> +) + +export default SwellMessage diff --git a/src/core/constants.ts b/src/core/constants.ts index c5ce2fe6..0bb79144 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -84,6 +84,7 @@ export const BASE_METADATA_URL = 'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/' export const MAX_APPS_DEFAULT = 12 +export const APP_SUBCATEGORY_MATCH_WEIGHT = 2 export const OFFCHAIN_APP = '__offchain' @@ -99,7 +100,13 @@ export const SKALE_SOCIAL_LINKS = { discord: 'https://discord.com/invite/gM5XBy6', github: 'https://github.com/skalenetwork', swell: 'https://swell.skale.space/', - website: 'https://skale.space/' + website: 'https://skale.space/', + dune: DUNE_SKALE_URL } export const DEFAULT_SWELL_URL = 'https://swell.skale.space/' +export const GET_STARTED_URL = 'https://skale.space/get-started-on-skale' + +export const DEFAULT_MIN_SFUEL_WEI = 100000000000000 +export const SFUEL_CHECK_INTERVAL = 10000 +export const DOCS_PORTAL_URL = 'https://docs.skale.space/' diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts index 6be51a4a..7ac94ec8 100644 --- a/src/core/ecosystem/apps.ts +++ b/src/core/ecosystem/apps.ts @@ -23,6 +23,9 @@ import { type types } from '@/core' import { getChainAlias } from '../metadata' +const SWELL_CHAIN = '__offchain' +const SWELL_APP = 'swell' + export function getAllApps(chainsMetadata: types.ChainsMetadataMap): types.AppWithChainAndName[] { const allApps: types.AppWithChainAndName[] = [] @@ -45,17 +48,30 @@ export function sortAppsByAlias(apps: types.AppWithChainAndName[]): types.AppWit return apps.sort((a, b) => a.alias.localeCompare(b.alias)) } +export function sortAndFilterApps(apps: types.AppWithChainAndName[]): types.AppWithChainAndName[] { + const swellAppIndex = apps.findIndex( + (app) => app.chain === SWELL_CHAIN && app.appName === SWELL_APP + ) + let swellApp: types.AppWithChainAndName | null = null + if (swellAppIndex !== -1) { + swellApp = apps.splice(swellAppIndex, 1)[0] + } + const sortedApps = apps.sort((a, b) => a.alias.localeCompare(b.alias)) + if (swellApp) { + sortedApps.unshift(swellApp) + } + return sortedApps +} + export function filterAppsByCategory( apps: types.AppWithChainAndName[], checkedItems: string[] ): types.AppWithChainAndName[] { - if (checkedItems.length === 0) return apps - return apps.filter((app) => { + if (checkedItems.length === 0) return sortAndFilterApps(apps) + const filteredApps = 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}` @@ -65,6 +81,7 @@ export function filterAppsByCategory( return false }) }) + return sortAndFilterApps(filteredApps) } export function filterAppsBySearchTerm( @@ -72,14 +89,15 @@ export function filterAppsBySearchTerm( searchTerm: string, chainsMeta: types.ChainsMetadataMap ): types.AppWithChainAndName[] { - if (!searchTerm || searchTerm === '') return apps + if (!searchTerm || searchTerm === '') return sortAndFilterApps(apps) const st = searchTerm.toLowerCase() - return apps.filter( + const filteredApps = apps.filter( (app) => app.alias.toLowerCase().includes(st) || app.chain.toLowerCase().includes(st) || getChainAlias(chainsMeta, app.chain).toLowerCase().includes(st) ) + return sortAndFilterApps(filteredApps) } export function getAppMeta( @@ -89,3 +107,17 @@ export function getAppMeta( ): types.AppMetadata | undefined { return chainsMeta[chain]?.apps?.[app] } + +export function getAppMetaWithChainApp( + chainsMeta: types.ChainsMetadataMap, + chain: string, + app: string +): types.AppWithChainAndName | undefined { + const meta = chainsMeta[chain]?.apps?.[app] + if (!meta) return undefined + return { + ...meta, + chain, + appName: app + } +} diff --git a/src/core/ecosystem/categories.ts b/src/core/ecosystem/categories.ts index ee5940e4..6b5412ff 100644 --- a/src/core/ecosystem/categories.ts +++ b/src/core/ecosystem/categories.ts @@ -88,7 +88,10 @@ export const categories: Categories = { wallet: { name: 'Wallet', subcategories: {} }, metaverse: { name: 'Metaverse', subcategories: {} }, web3: { name: 'Web3', subcategories: {} }, - pretge: { name: 'Pre-TGE', subcategories: {} } + pretge: { name: 'Pre-TGE', subcategories: {} }, + utility: { name: 'Utility', subcategories: {} }, + analytics: { name: 'Analytics', subcategories: {} }, + validator: { name: 'Validator', subcategories: {} } } export const sortCategories = (categories: Categories): Categories => { diff --git a/src/core/ecosystem/similarApps.ts b/src/core/ecosystem/similarApps.ts new file mode 100644 index 00000000..3b83d4d6 --- /dev/null +++ b/src/core/ecosystem/similarApps.ts @@ -0,0 +1,94 @@ +/** + * @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 similarApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { type types } from '@/core' +import { APP_SUBCATEGORY_MATCH_WEIGHT, MAX_APPS_DEFAULT } from '../constants' + +interface CategoryMatch { + categoryMatches: number + subcategoryMatches: number + totalScore: number +} + +interface SimilarApp extends types.AppWithChainAndName { + score: number +} + +function calculateCategoryMatches( + sourceApp: types.AppWithChainAndName, + targetApp: types.AppWithChainAndName +): CategoryMatch { + let categoryMatches = 0 + let subcategoryMatches = 0 + + Object.entries(sourceApp.categories).forEach(([category, sourceSubcategories]) => { + if (category in targetApp.categories) { + categoryMatches++ + const targetSubcategories = targetApp.categories[category] + if (Array.isArray(sourceSubcategories) && Array.isArray(targetSubcategories)) { + const matches = sourceSubcategories.filter((sub) => + targetSubcategories.includes(sub) + ).length + subcategoryMatches += matches + } + } + }) + + const totalScore = categoryMatches + subcategoryMatches * APP_SUBCATEGORY_MATCH_WEIGHT + return { categoryMatches, subcategoryMatches, totalScore } +} + +function isAppInList(app: types.AppWithChainAndName, list: types.AppWithChainAndName[]): boolean { + return list.some((item) => item.chain === app.chain && item.appName === app.appName) +} + +function findSimilarApps( + currentApp: types.AppWithChainAndName | undefined, + allApps: types.AppWithChainAndName[], + favoriteApps: types.AppWithChainAndName[] = [], + limit: number = MAX_APPS_DEFAULT +): SimilarApp[] { + const apps = allApps.filter((app) => { + if (currentApp && app.chain === currentApp.chain && app.appName === currentApp.appName) { + return false + } + return !isAppInList(app, favoriteApps) + }) + + const targetApps = currentApp ? [currentApp] : favoriteApps + if (!targetApps.length) return [] + + const appScores: SimilarApp[] = apps.map((app) => { + const scores = targetApps.map((targetApp) => calculateCategoryMatches(targetApp, app)) + + const maxScore = Math.max(...scores.map((s) => s.totalScore)) + return { ...app, score: maxScore } + }) + + return appScores + .filter((app) => app.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, limit) +} + +export { findSimilarApps, type SimilarApp } diff --git a/src/core/ecosystem/utils.ts b/src/core/ecosystem/utils.ts index c63f232a..f8975b3f 100644 --- a/src/core/ecosystem/utils.ts +++ b/src/core/ecosystem/utils.ts @@ -88,3 +88,11 @@ export const isNewApp = ( ): boolean => { return newApps.some((newApp) => newApp.chain === app.chain && newApp.appName === app.app) } + +export const isTrending = ( + apps: types.AppWithChainAndName[], + chainName: string, + appName: string +): boolean => { + return apps.some((a) => a.appName === appName && a.chain === chainName) +} diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 3acb773c..57f2489a 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -41,7 +41,10 @@ export function getTotalAppCounters( gas_usage_count: '0', token_transfers_count: '0', transactions_count: '0', - validations_count: '0' + validations_count: '0', + transactions_today: 0, + transactions_last_7_days: 0, + transactions_last_30_days: 0 } for (const address in countersArray) { if (countersArray.hasOwnProperty(address)) { @@ -60,6 +63,9 @@ export function getTotalAppCounters( totalCounters.validations_count = ( parseInt(totalCounters.validations_count) + parseInt(addressCounters.validations_count) ).toString() + totalCounters.transactions_today += addressCounters.transactions_today + totalCounters.transactions_last_7_days += addressCounters.transactions_last_7_days + totalCounters.transactions_last_30_days += addressCounters.transactions_last_30_days } } return totalCounters diff --git a/src/core/meta.ts b/src/core/meta.ts index 41cd6d22..c89d8b25 100644 --- a/src/core/meta.ts +++ b/src/core/meta.ts @@ -29,11 +29,13 @@ export const META_TAGS = { }, stats: { title: 'SKALE Portal - Network Stats', - description: 'SKALE Network statistics - Transactions, active users, gas fees saved.' + description: 'SKALE Network statistics - Transactions, active users, gas fees saved.', + help: 'Check live SKALE Network statistics like active wallets, transactions, and total gas fees saved by users.' }, bridge: { title: 'SKALE Portal - Bridge Tokens', - description: 'Bridge tokens using SKALE Bridge - Zero Gas Fees between SKALE Chains.' + description: 'Bridge tokens using SKALE Bridge - Zero Gas Fees between SKALE Chains.', + help: 'Transfer your assets to the SKALE Network and move easily across SKALE Chains to access a variety of dApps.' }, history: { title: 'SKALE Portal - Bridge History', @@ -47,11 +49,13 @@ export const META_TAGS = { chains: { title: 'SKALE Portal - Chains', description: - 'Explore SKALE Hubs, AppChains, connect to SKALE Chains, get block explorer links, endpoints, linked tokens and verified contracts info.' + 'Explore SKALE Hubs, AppChains, connect to SKALE Chains, get block explorer links, endpoints, linked tokens and verified contracts info.', + help: 'SKALE Chains are custom blockchains within the SKALE Network designed to power specific dAps and use cases.' }, - apps: { - title: 'SKALE Portal - Apps', - description: 'Apps on SKALE Network. Explore and interact with dApps on SKALE Network.' + ecosystem: { + title: 'SKALE Portal - Ecosystem', + description: 'Explore and interact with dApps on SKALE Network.', + help: 'Discover and explore all the dApps available across the SKALE Network in one place.' }, faq: { title: 'SKALE Portal - Bridge FAQ', @@ -59,10 +63,17 @@ export const META_TAGS = { }, staking: { title: 'SKALE Portal - Staking', - description: 'Delegate, review delegations and withdraw staking rewards' + description: 'Delegate, review delegations and withdraw staking rewards', + help: 'Delegate your SKL tokens to help secure the SKALE Network and earn rewards.' + }, + validators: { + title: 'SKALE Portal - Validators', + description: 'List of validators on SKALE Network', + help: 'Explore validators that secure the SKALE Network and choose your preferred ones to stake SKL tokens with to earn rewards.' }, onramp: { title: 'SKALE Portal - Onramp', - description: 'Purchase crypto directly on SKALE Europa Hub using the Transak onramp.' + description: 'Purchase crypto directly on SKALE Europa Hub using the Transak onramp.', + help: 'Use your preferred fiat currency to get USDC directly on the SKALE Europa Hub.' } } diff --git a/src/core/metadata.ts b/src/core/metadata.ts index 65a5c237..3b6cb54c 100644 --- a/src/core/metadata.ts +++ b/src/core/metadata.ts @@ -22,6 +22,7 @@ import { type types } from '@/core' import { BASE_METADATA_URL, MAINNET_CHAIN_NAME } from './constants' +import { AppMetadata } from '@/core/dist/types' export function chainBg( chainsMeta: types.ChainsMetadataMap, @@ -69,3 +70,12 @@ export async function loadMeta(skaleNetwork: types.SkaleNetwork): Promise= appMeta.pretge.from && now <= appMeta.pretge.to + } + if ('pretge' in appMeta.categories) return true + return false +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 1921f2d2..1e5a1ad1 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -48,29 +48,36 @@ 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 HourglassBottomRoundedIcon from '@mui/icons-material/HourglassBottomRounded' +import HourglassTopRoundedIcon from '@mui/icons-material/HourglassTopRounded' +import HourglassFullRoundedIcon from '@mui/icons-material/HourglassFullRounded' +import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded' -import ChainLogo from '../components/ChainLogo' -import SkStack from '../components/SkStack' -import Tile from '../components/Tile' -import LinkSurface from '../components/LinkSurface' -import Breadcrumbs from '../components/Breadcrumbs' -import CollapsibleDescription from '../components/CollapsibleDescription' -import HubTile from '../components/chains/HubTile' -import AccordionSection from '../components/AccordionSection' +import { useApps } from '../useApps' import { findChainName } from '../core/chain' - +import { getAppMetaWithChainApp } from '../core/ecosystem/apps' import { formatNumber } from '../core/timeHelper' -import { chainBg, getChainAlias } from '../core/metadata' +import { chainBg, getChainAlias, isPreTge } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' import { MAINNET_CHAIN_LOGOS, MAX_APPS_DEFAULT, OFFCHAIN_APP } from '../core/constants' +import { getRecentApps, isNewApp, isTrending } from '../core/ecosystem/utils' + import SocialButtons from '../components/ecosystem/Socials' import CategoriesChips 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' +import AppScreenshots from '../components/ecosystem/AppScreenshots' +import RecommendedApps from '../components/ecosystem/RecommendedApps' +import ChainLogo from '../components/ChainLogo' +import Tile from '../components/Tile' +import LinkSurface from '../components/LinkSurface' +import Breadcrumbs from '../components/Breadcrumbs' +import CollapsibleDescription from '../components/CollapsibleDescription' +import HubTile from '../components/chains/HubTile' +import AccordionSection from '../components/AccordionSection' export default function App(props: { mpc: MetaportCore @@ -80,15 +87,7 @@ export default function App(props: { chainsMeta: types.ChainsMetadataMap }) { let { chain, app } = useParams() - const { - likedApps, - appLikes, - toggleLikedApp, - getAppId, - getTrendingApps, - refreshLikedApps, - getTrendingRank - } = useLikedApps() + const { likedApps, appLikes, toggleLikedApp, getAppId, refreshLikedApps } = useLikedApps() const { isSignedIn, handleSignIn } = useAuth() const { address } = useWagmiAccount() @@ -126,13 +125,14 @@ export default function App(props: { const appDescription = appMeta.description ?? 'No description' + const { trendingApps, allApps } = useApps(props.chainsMeta, props.metrics) + const appId = getAppId(chain, app) const isLiked = likedApps.includes(appId) const likesCount = appLikes[appId] || 0 - const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) - const trendingIndex = getTrendingRank(trendingAppIds, appId) const isNew = isNewApp({ chain, app }, newApps) + const trending = isTrending(trendingApps, chain, app) const handleToggleLike = async () => { if (!address) { @@ -242,9 +242,9 @@ export default function App(props: {

{appAlias}

- {trendingIndex !== undefined && } + {trending && } {isNew && } - {appMeta.tags?.includes('pretge') && } + {isPreTge(appMeta) && }
@@ -255,41 +255,84 @@ export default function App(props: {
- - {appMeta.contracts ? ( - } - /> - ) : null} - {appMeta.contracts ? ( + + {appMeta.contracts && ( + + } + /> + + )} + {appMeta.contracts && ( + + + ) : undefined + } + tooltip={ + props.metrics && counters + ? `Given gas price ${props.metrics.gas} Gwei. ${counters.gas_usage_count} of gas used.` + : undefined + } + value={props.metrics && counters ? `${formatGas()} ETH` : undefined} + icon={} + /> + + )} + - ) : undefined - } - tooltip={ - props.metrics && counters - ? `Given gas price ${props.metrics.gas} Gwei. ${counters.gas_usage_count} of gas used.` - : undefined - } - value={props.metrics && counters ? `${formatGas()} ETH` : undefined} - icon={} + text="Favorites" + value={likesCount.toString()} + icon={} /> - ) : null} - } - /> - + + {appMeta.contracts && ( + + } + /> + + )} + {appMeta.contracts && ( + + } + /> + + )} + {appMeta.contracts && ( + + } + /> + + )} + + {chain !== OFFCHAIN_APP && ( )} + + + } + marg={false} + > + + +
) diff --git a/src/pages/Bridge.tsx b/src/pages/Bridge.tsx index 53c21625..53c90f26 100644 --- a/src/pages/Bridge.tsx +++ b/src/pages/Bridge.tsx @@ -24,7 +24,8 @@ import { Helmet } from 'react-helmet' import { useEffect, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { Link, useSearchParams } from 'react-router-dom' +import HistoryIcon from '@mui/icons-material/History' import { CHAINS_META, @@ -34,7 +35,8 @@ import { useMetaportStore, SkPaper, type interfaces, - TransactionData + TransactionData, + styles } from '@skalenetwork/metaport' import { type types } from '@/core' @@ -45,6 +47,8 @@ import BridgeBody from '../components/BridgeBody' import { META_TAGS } from '../core/meta' import Meson from '../components/Meson' +import { Button } from '@mui/material' +import SkPageInfoIcon from '../components/SkPageInfoIcon' interface TokenParams { keyname: string | null @@ -163,10 +167,27 @@ export default function Bridge(props: { isXs: boolean; chainsMeta: types.ChainsM -
-

Transfer

+
+
+

Bridge

+

+ Zero Gas Fees between SKALE Chains +

+
+
+ + + + +
-

Zero Gas Fees between SKALE Chains

+
{transactionsHistory.length !== 0 ? ( diff --git a/src/pages/Chain.tsx b/src/pages/Chain.tsx index f0ed90ad..1a8bf6de 100644 --- a/src/pages/Chain.tsx +++ b/src/pages/Chain.tsx @@ -34,7 +34,7 @@ import { type types } from '@/core' import { findChainName } from '../core/chain' export default function Chain(props: { - loadData: any + loadData: () => Promise schains: types.ISChain[] stats: types.IStats | null metrics: types.IMetrics | null diff --git a/src/pages/Chains.tsx b/src/pages/Chains.tsx index 5d2ef631..0a32382a 100644 --- a/src/pages/Chains.tsx +++ b/src/pages/Chains.tsx @@ -39,6 +39,7 @@ import CategoryRoundedIcon from '@mui/icons-material/CategoryRounded' import ChainsSection from '../components/chains/ChainsSection' import { META_TAGS } from '../core/meta' import { MAINNET_CHAIN_NAME } from '../core/constants' +import SkPageInfoIcon from '../components/SkPageInfoIcon' export default function Chains(props: { loadData: () => Promise @@ -92,13 +93,15 @@ export default function Chains(props: { -
-

SKALE Chains

+
+
+

SKALE Chains

+

+ Connect, get block explorer links and endpoints +

+
+
-

- Connect, get block explorer links and endpoints -

- Promise }) { const { getCheckedItemsFromUrl, setCheckedItemsInUrl, getTabIndexFromUrl, setTabIndexInUrl } = useUrlParams() - const { allApps, newApps, trendingApps, favoriteApps, isSignedIn } = useApps(props.chainsMeta) + const { allApps, newApps, trendingApps, favoriteApps, isSignedIn } = useApps( + props.chainsMeta, + props.metrics + ) const [checkedItems, setCheckedItems] = useState([]) const [filteredApps, setFilteredApps] = useState([]) @@ -63,6 +69,7 @@ export default function Ecosystem(props: { const [loaded, setLoaded] = useState(false) useEffect(() => { + props.loadData() const initialCheckedItems = getCheckedItemsFromUrl() setCheckedItems(initialCheckedItems) const initialTabIndex = getTabIndexFromUrl() @@ -102,6 +109,14 @@ export default function Ecosystem(props: { ], // New Apps [ 2, + trendingApps.filter((app) => + filteredApps.some( + (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.appName + ) + ) + ], // Trending Apps + [ + 3, isSignedIn ? favoriteApps.filter((app) => filteredApps.some( @@ -110,42 +125,39 @@ export default function Ecosystem(props: { ) ) : [] - ], // Favorite Apps - [ - 3, - trendingApps.filter((app) => - filteredApps.some( - (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.appName - ) - ) - ] // Trending Apps + ] // Favorite Apps ]) return (tabIndex: number) => filterMap.get(tabIndex) || filteredApps - }, [filteredApps, newApps, favoriteApps, trendingApps, isSignedIn]) + }, [filteredApps, newApps, trendingApps, favoriteApps, isSignedIn]) const currentFilteredApps = getFilteredAppsByTab(activeTab) + const isFiltersApplied = Object.keys(checkedItems).length !== 0 + return ( - {META_TAGS.apps.title} - - - + {META_TAGS.ecosystem.title} + + + -
+

Ecosystem

Explore dApps across the SKALE ecosystem

-
- +
+ +
+ +
-
+
} + label="Trending" + icon={} iconPosition="start" className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} /> } + label="Favorites" + icon={} iconPosition="start" className={cls('btn', 'btnSm', cmn.mri5, cmn.mleft5, 'tab', 'fwmobile')} /> @@ -209,6 +216,7 @@ export default function Ecosystem(props: { chainsMeta={props.chainsMeta} newApps={newApps} loaded={loaded} + trendingApps={trendingApps} /> )} {activeTab === 1 && ( @@ -216,24 +224,26 @@ export default function Ecosystem(props: { newApps={currentFilteredApps} skaleNetwork={props.mpc.config.skaleNetwork} chainsMeta={props.chainsMeta} + trendingApps={trendingApps} /> )} {activeTab === 2 && ( - )} {activeTab === 3 && ( - )} diff --git a/src/pages/History.tsx b/src/pages/History.tsx index c78162a0..b83e742c 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -35,8 +35,12 @@ import { useMetaportStore } from '@skalenetwork/metaport' +import HistoryIcon from '@mui/icons-material/History' +import ArrowBackIosNewRoundedIcon from '@mui/icons-material/ArrowBackIosNewRounded' + import { setHistoryToStorage } from '../core/transferHistory' import { META_TAGS } from '../core/meta' +import Breadcrumbs from '../components/Breadcrumbs' export default function History() { const mpc = useMetaportStore((state) => state.mpc) @@ -58,16 +62,24 @@ export default function History() { -
-
-

- History ({transfersHistory.length + (transactionsHistory.length !== 0 ? 1 : 0)}) -

-

SKALE Bridge transfers history

-
-
+
+ , + url: '/bridge' + }, + { + text: 'History', + icon: + } + ]} + /> +
-
+
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 6be86a87..da77a7fb 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,6 +1,30 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file Home.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useEffect } from 'react' import { Link } from 'react-router-dom' import { Container, Stack, Box, Grid, Button } from '@mui/material' -import { cmn, cls } from '@skalenetwork/metaport' +import { cmn, cls, SkPaper } from '@skalenetwork/metaport' import { type types } from '@/core' import { useApps } from '../useApps' @@ -8,19 +32,33 @@ import { useApps } from '../useApps' import Headline from '../components/Headline' import PageCard from '../components/PageCard' import CategoryCardsGrid from '../components/ecosystem/CategoryCardsGrid' -import NewApps from '../components/ecosystem/NewApps' -import FavoriteApps from '../components/ecosystem/FavoriteApps' -import TrendingApps from '../components/ecosystem/TrendingApps' +import NewApps from '../components/ecosystem/tabs/NewApps' +import FavoriteApps from '../components/ecosystem/tabs/FavoriteApps' +import TrendingApps from '../components/ecosystem/tabs/TrendingApps' +import { SKALE_SOCIAL_LINKS } from '../core/constants' import { SECTION_ICONS, EXPLORE_CARDS } from '../components/HomeComponents' +import SocialButtons from '../components/ecosystem/Socials' +import UserRecommendations from '../components/ecosystem/UserRecommendations' interface HomeProps { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap + metrics: types.IMetrics | null + loadData: () => Promise } -export default function Home({ skaleNetwork, chainsMeta }: HomeProps): JSX.Element { - const { newApps, trendingApps, favoriteApps, isSignedIn } = useApps(chainsMeta) +export default function Home({ + skaleNetwork, + chainsMeta, + metrics, + loadData +}: HomeProps): JSX.Element { + const { newApps, trendingApps, favoriteApps, isSignedIn } = useApps(chainsMeta, metrics) + + useEffect(() => { + loadData() + }, []) return ( @@ -35,7 +73,7 @@ export default function Home({ skaleNetwork, chainsMeta }: HomeProps): JSX.Eleme } /> + } /> } @@ -82,6 +127,13 @@ export default function Home({ skaleNetwork, chainsMeta }: HomeProps): JSX.Eleme className={cls(cmn.mbott10, cmn.mtop20, cmn.ptop20)} /> +
+
+ + + +
+
) } diff --git a/src/pages/Onramp.tsx b/src/pages/Onramp.tsx index 3c2239e1..fef05652 100644 --- a/src/pages/Onramp.tsx +++ b/src/pages/Onramp.tsx @@ -38,6 +38,7 @@ import { META_TAGS } from '../core/meta' import TokenBalanceTile from '../components/TokenBalanceTile' import ConnectWallet from '../components/ConnectWallet' import Message from '../components/Message' +import SkPageInfoIcon from '../components/SkPageInfoIcon' import { getPaymasterChain } from '../core/paymaster' import { MAINNET_CHAIN_NAME, @@ -135,10 +136,15 @@ export default function Onramp(props: { mpc: MetaportCore }) { -

On-ramp

-

- Transfer your assets to SKALE Europa Hub -

+
+
+

On-ramp

+

+ Transfer your assets to SKALE Europa Hub +

+
+ +
{!isProd ? (
-
+
{loading !== false || props.customAddress !== undefined ? (
+
diff --git a/src/pages/Stats.tsx b/src/pages/Stats.tsx index 021b98b1..73e85405 100644 --- a/src/pages/Stats.tsx +++ b/src/pages/Stats.tsx @@ -29,6 +29,7 @@ import { cmn, cls } from '@skalenetwork/metaport' import { DASHBOARD_URL } from '../core/constants' import { META_TAGS } from '../core/meta' +import SkPageInfoIcon from '../components/SkPageInfoIcon' export default function Stats() { return ( @@ -40,12 +41,14 @@ export default function Stats() { -
-

Stats

+
+
+

Stats

+

SKALE Network statistics

+
+
-

- SKALE Network statistics -

+